Search code examples
javacollectionsjava-stream

How to stream and collect multiple max items based on a specific fields


List<Car> carList = new ArrayList<>();
Car car = new Car();
car.setMake("Honda");
car.setYear(1998);
car.setColor("red");
carList.add(car);

car = new Car();
car.setMake("Honda");
car.setYear(2020);
car.setColor("red");
carList.add(car);

car = new Car();
car.setMake("Audi");
car.setYear(2022);
car.setColor("red");
carList.add(car);

car = new Car();
car.setMake("Toyota");
car.setYear(2021);
car.setColor("blue");
carList.add(car);

car = new Car();
car.setMake("Toyota");
car.setYear(2007);
car.setColor("blue");
carList.add(car);

How to stream and collect to a map with color as key and value has list of cars with oldest car by make?

The final Map should be:

{
  "red": [
    {
      "make": "Honda",
      "year": "1998"
    },
    {
      "make": "Audi",
      "year": "2022"
    }
  ],
  "blue": [
    {
      "make": "Toyota",
      "year": "2007"
    }
  ]
}

Once the groupingBy is used to group based on color, having issue to reduce the list (values) to oldest car by make and then collect to a map as whole.

I was able to get the expected result by :

Map<String, Map<String, Optional<Car>>> carMap =
                carList.stream().collect(Collectors.groupingBy(Car::getColor,
                        Collectors.groupingBy(Car::getMake,
                                Collectors.minBy(Comparator.comparing(Car::getYear)))));
{red={Audi=Optional[Car(make=Audi, year=2022, color=red)], Honda=Optional[Car(make=Honda, year=1998, color=red)]}, blue={Toyota=Optional[Car(make=Toyota, year=2007, color=blue)]}}

But unable to flat map the values in the nested map to a list of Cars in the parent map.


Solution

  • Basically, you should just have to map the collected values:

    cars.stream().collect(Collectors.groupingBy(Car::color,
        Collectors.collectingAndThen(
            Collectors.groupingBy(Car::make,
                Collectors.collectingAndThen(
                    Collectors.minBy(Comparator.comparing(Car::year)), Optional::get)
            ), map -> new ArrayList<>(map.values()))));
    

    But really, this is where streams get unreadable...

    Update

    To write this in at least a little more readable fashion:

    Collector<Car, ?, Car> eldest = Collectors.collectingAndThen(
            Collectors.minBy(Comparator.comparing(Car::year)),
            Optional::get);
    
    Collector<Car, ?, List<Car>> eldestByMake = Collectors.collectingAndThen(
            Collectors.groupingBy(Car::make, eldest), 
            map -> new ArrayList<>(map.values()));
    
    cars.stream().collect(Collectors.groupingBy(Car::color, eldestByMake));