I have the following domain classes Trip
and Employee
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Trip {
private Date startTime;
private Date endTime;
List<Employee> empList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
private String name;
private String empId;
}
I have a list of Trip
instances. And I want to create a map of type Map<String,List<Trip>>
associating id of each employee empId
with a list of trips using Stream API.
Here's my attempt:
public static void main(String[] args) {
List<Trip> trips = new ArrayList<>();
Map<Stream<String>, List<Trip>> x = trips.stream()
.collect(Collectors.groupingBy(t -> t.getEmpList()
.stream().map(Employee::getEmpId)
));
}
How can I generate the map of the required type?
When the type of map is Map<String,List<Trip>>
it gives me a compilation error:
Unresolved compilation problem: Type mismatch:
cannot convert from Map<Object,List<Trip>> to Map<String,List<Trip>>
CodePudding user response:
To group the data by the property of a nested object and at the same time preserve a link to the enclosing object, you need to flatten the stream using an auxiliary object that would hold references to both employee id and enclosing Trip
instance.
A Java 16 record
would fit into this role perfectly well. If you're using an earlier JDK version, you can implement it a plain class
(a quick and dirty way would be to use Map.Entry
, but it decreases the readability, because of the faceless methods getKey()
and getValue()
require more effort to reason about the code). I will go with a record
, because this option is the most convenient.
The following line is all we need (the rest would be automatically generated by the compiler):
public record TripEmployee(String empId, Trip trip) {}
The first step is to flatten the stream data and turn the Stream<Trip>
into Stream<TripEmployee>
. Since it's one-to-many transformation, we need to use flatMap()
operation to turn each Employee
instance into a TripEmployee
.
And then we need to apply collect. In order to generate the resulting Map, we can make use of the collector groupingBy()
with collector mapping()
as a downstream. In collector mapping
always requires a downstream collector and this case we need to provide toList()
.
List<Trip> trips = // initializing the list
Map<String, List<Trip>> empMap = trips.stream()
.flatMap(trip -> trip.getEmployee().stream()
.map(emp -> new TripEmployee(emp.getEmpId(), trip))
)
.collect(Collectors.groupingBy(
TripEmployee::empId,
Collectors.mapping(TripEmployee::trip,
Collectors.toList())
));
CodePudding user response:
Not sure which Java version you are using but since you have mentioned Stream, I will assume Java 8 at least.
Second assumption, not sure why but looking at your code (using groupingBy
) you want the whole List<Trip>
which you get against an empId
in a Map
.
To have the better understanding first look at this code (without Stream
):
public Map<String, List<Trip>> doSomething(List<Trip> listTrip) {
List<Employee> employeeList = new ArrayList<>();
for (Trip trip : listTrip) {
employeeList.addAll(trip.getEmployee());
}
Map<String, List<Trip>> stringListMap = new HashMap<>();
for (Employee employee : employeeList) {
stringListMap.put(employee.getEmpId(), listTrip);
}
return stringListMap;
}
You can see I pulled an employeeList
first , reason being your use case. And now you can see how easy was to create a map out of it.
You may use Set
instead of List
if you're worried about the duplicates.
So with StreamApi
above code could be:
public Map<String, List<Trip>> doSomethingInStream(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip));
}
You can take care of duplicates while creating map as well, as:
public Map<String, List<Trip>> doSomething3(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip, (oldValue, newValue) -> newValue));
}
Like the first answer says, if you are Java 16 using record
will ease your task a lot in terms of model definition.