Jha
Jha

Reputation: 11

Generate Map using list of list using stream in Java 8

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

Answers (3)

iamgirdhar
iamgirdhar

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,

  • First I have created an additional list of EmployeeTripMapping object with Trip data corresponding to the empId by iterating the listOfTrips.
  • I have used 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:

  1. 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

  2. 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

Alexander Ivanchenko
Alexander Ivanchenko

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

Vivek
Vivek

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

Related Questions