Reputation: 11
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>>
Upvotes: 0
Views: 2478
Reputation: 1155
Using Java 8 stream
You can use the below approach to get the desired results using stream function groupingBy
.
Since you have mentioned above to use java 8, so my solution is inclined to java 8 itself.
Logic:
Here,
EmployeeTripMapping
object
with Trip
data corresponding to the empId
by iterating the
listOfTrips
.Collectors.groupingBy
on the List<EmployeeTripMapping>
and grouped the data based on the empId
and using Collectors.mapping
collect the list of Trip corresponding to the empId.Few Suggestions:
Records in java 14 : As I can see in your problem statement, you are using lombok annotations to create getters, setters and constructors, so instead of that we can replace our data classes with records. Records are immutable classes that require only the type and name of fields. We do not need to create constructor, getters, setters, override toString() methods, override hashcode and equals methods. Here
JavaTimeAPI in java 8: Instead of Date, you can use LocalDateTime available in java time API in java 8. Here
Code:
public class Test {
public static void main(String[] args) {
Trip t1 = new Trip(LocalDateTime.of(2022,10,28,9,00,00),
LocalDateTime.of(2022,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t2 = new Trip(LocalDateTime.of(2021,10,28,9,00,00),
LocalDateTime.of(2021,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t3 = new Trip(LocalDateTime.of(2020,10,28,9,00,00),
LocalDateTime.of(2020,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
Trip t4 = new Trip(LocalDateTime.of(2019,10,28,9,00,00),
LocalDateTime.of(2019,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
List<Trip> listOfTrips = Arrays.asList(t1,t2,t3,t4);
List<EmployeeTripMapping> empWithTripMapping = new ArrayList<>();
listOfTrips.forEach(x -> x.getEmpList().forEach(y ->
empWithTripMapping.add(new EmployeeTripMapping(y.getEmpId(),x))));
Map<String,List<Trip>> employeeTripGrouping = empWithTripMapping.stream()
.collect(Collectors.groupingBy(EmployeeTripMapping::getEmpId,
Collectors.mapping(EmployeeTripMapping::getTrip,
Collectors.toList())));
System.out.println(employeeTripGrouping);
}
}
EmployeeTripMapping.java
public class EmployeeTripMapping {
private String empId;
private Trip trip;
//getters and setters
}
Output:
{emp2=[Trip{startTime=2020-10-28T09:00, endTime=2020-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}, Trip{startTime=2019-10-28T09:00, endTime=2019-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}],
emp1=[Trip{startTime=2022-10-28T09:00, endTime=2022-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}, Trip{startTime=2021-10-28T09:00, endTime=2021-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}]}
Upvotes: 0
Reputation: 29058
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.getEmpList().stream()
.map(emp -> new TripEmployee(emp.getEmpId(), trip))
)
.collect(Collectors.groupingBy(
TripEmployee::empId,
Collectors.mapping(TripEmployee::trip,
Collectors.toList())
));
A Java 8 compliant solution is available via this Link
Upvotes: 2
Reputation: 1125
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.
Upvotes: 0