Search code examples
javajava-8java-streammethod-referencecollect

Grouping a map by object's property to a new Map


First things first, let me add the actual "example code":

Map<CarBrand, List<Car>> allCarsAndBrands = new HashMap();

final String bmwBrandName = "BMW";
final String audiBrandName = "AUDI";

List<Car> bmwCars = new ArrayList();
bmwCars.add(new Car(CarType.FAST, "Z4", "silver", bmwBrandName));
bmwCars.add(new Car(CarType.FAST, "M3", "red", bmwBrandName));
bmwCars.add(new Car(CarType.SLOW, "X1", "black", bmwBrandName));

List<Car> audiCars = new ArrayList();
audiCars.add(new Car(CarType.FAST, "S3", "yellow", audiBrandName));
audiCars.add(new Car(CarType.FAST, "R8", "silver", audiBrandName));
audiCars.add(new Car(CarType.SLOW, "A1", "white", audiBrandName));

allCarsAndBrands.put(new CarBrand(bmwBrandName), bmwCars);
allCarsAndBrands.put(new CarBrand(audiBrandName), audiCars);

Map<CarType, Map<CarBrand, List<Car>>> mappedCars;

Problem

My goal on this is to populate mappedCars by CarType, which would result in two big sets: one containing all FAST cars and the other all SLOW cars (or any future "types", each one having the previous map structure with CarBrand and the related cars).

I'm currently failing to find the proper use of Collections/Streams for this "map with lists inside other map". I've had other cases with simple maps/lists but this one is proving to be trickier for me.

Attempts

Here's an initial code "attempt":

mappedCars = allCarsAndBrands.entrySet()
                             .stream()
                             .collect(
                               groupingBy(Car::getType, 
                                 groupingBy(Map.Entry::getKey)
                               )
                             );

I'm also getting the "non-static cannot be referenced error" (Map.Entry::getKey) but this is due the fact that I'm failing to match the actual expected return (Static context cannot access non-static in Collectors)

I'm simply confused at this point, tried using Collectors.toMap too but still can't get a working grouping.

Extras

Here are the class definitions for this example:

class CarBrand {
   CarBrand(String name) {
      this.name = name;
   }
   String name;
}

class Car {
    Car(CarType type, String name, String color, String brandName) {
        this.type = type;
        this.name = name;
        this.color = color;
        this.brandName = brandName;
    }

    public CarType getType() {
        return type;
    }

    CarType type;
    String name;
    String color;
    String brandName;
}

enum CarType {
   FAST,
   SLOW,
}

EDIT: "DIRTY" SOLUTION

Here's a "hackish" solution (based on the comments suggestions, will check the answers!):

Map<CarType, Map<CarBrand, List<Car>>> mappedCars = allCarsAndBrands
                .values()
                .stream()
                .flatMap(List::stream)
                .collect(Collectors.groupingBy(
                        Car::getType,
                        Collectors.groupingBy(
                                car -> allCarsAndBrands.keySet().stream().filter(brand -> brand.name == car.brandName).findFirst().get(),
                                Collectors.toList()
                        )
                ));

As mentioned in the comments (should've added here before), there's a "business constraint" that adds some limitations for the solution. I also didn't feel like creating a new CarBrand since in the real world that's not that simple as seen on this... but again, using the original map and filtering + find is just bad.


Solution

  • With the use of existing models, and the initial approach of nested grouping you were thinking in the right direction. The improvement could be made in thinking about flattening the value part of the Map while iterating over the entries.

    allCarsAndBrands.entrySet().stream()
            .flatMap(e -> e.getValue().stream()
                    .map(car -> new AbstractMap.SimpleEntry<>(e.getKey(), car)))
    

    Once you have that, the grouping concept works pretty much the same, but now the default returned grouped values would instead be of the entry type. Hence a mapping is further required. This leaves the overall solution to be something like :

    Map<CarType, Map<CarBrand, List<Car>>> mappedCars =
            allCarsAndBrands.entrySet().stream()
                    .flatMap(e -> e.getValue().stream()
                            .map(car -> new AbstractMap.SimpleEntry<>(e.getKey(), car)))
                    .collect(Collectors.groupingBy(e -> e.getValue().getType(),
                            Collectors.groupingBy(Map.Entry::getKey,
                                    Collectors.mapping(Map.Entry::getValue,
                                            Collectors.toList()))));