0

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.

dNurb
  • 385
  • 2
  • 16
  • 1
    [Why not upload images of code/errors when asking a question?](https://meta.stackoverflow.com/q/285551/5221149) – Andreas Sep 10 '20 at 21:16
  • A `Car` knows its "model" but not its "make" ("brand")? You should start by fixing that. – Andreas Sep 10 '20 at 21:18
  • 1
    Now that you have brand in the `Car` object, it's easy: `allCarsAndBrands.values().stream().flatMap(List::stream).collect(Collectors.groupingBy(Car::getType, Collectors.groupingBy(Car::getBrand, Collectors.toList())))` – Andreas Sep 10 '20 at 21:40
  • I fail to understand: you want all FAST and SLOW car, without caring for the brand, no? Would `Stream.concat(bmwCars, audiCars).collect(groupingBy(Car::getCarType))` be enough ? – NoDataFound Sep 10 '20 at 21:50
  • Thanks @Andreas! That almost did the trick but doesn't match the required type (see the last line of the first code block), this will return `>>`. Wondering if ::getBrand being the actual class/object would help.. but that's an actual constraint that I have. The only "link" between car and car brand is just the name/id – dNurb Sep 10 '20 at 21:51
  • @NoDataFound I do care about the brand (wish that wasn't the case). I need it to be just like the first map but with the extra grouping. – dNurb Sep 10 '20 at 21:53
  • So?!? Fix it! Either replace field `String brandName` with `CarBrand brand`, or create a `CarBrand` on the fly by replacing `Car::getBrand` with `c -> new CarBrand(c.getBrandname())` – Andreas Sep 10 '20 at 21:57
  • Thanks for the reply @Andreas! As mentioned in my previous comment, there's a "constraint" (a business one) that prevents me from having the `CarBrand` inside `Car`. Creating a new one on the fly is possible yes (I could also fetch it from a separate list by the name/id), but I'm trying to find a solution that utilizes the `CarBrand` from the original map while creating the new one. Do you think it's possible to re-use the `CarBrand` from the "groupingBy"? – dNurb Sep 10 '20 at 22:18
  • 1
    Sure, if you don't want to put it in the `Car` where it belong, create a `Map` and look it up. I don't see the problem. – Andreas Sep 11 '20 at 03:25

2 Answers2

1

As was discussed in the comments, this was simple to do if one includes the Make as a field of the Car class.

Based on your last comment, the easiest way was to use a hybrid solution using Stream and other features of the Map interface introduced in Java 8.

Data

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"));
bmwCars.add(new Car(CarType.FAST, "M3", "red"));
bmwCars.add(new Car(CarType.SLOW, "X1", "black"));

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

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

Process

This works by creating a Map<CarType, List<Car>> for each CarBrand and then reversing the keys. The only new feature you may be unfamiliar with is computeIfAbsent

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

allCarsAndBrands.forEach((brand, carList) -> {
    Map<CarType, List<Car>> typeMap = carList.stream()
            .collect(Collectors.groupingBy(Car::getType));
    typeMap.forEach((type, lst) -> {
        map.computeIfAbsent(type, value->
                new HashMap<CarBrand, List<Car>>())
                    .computeIfAbsent(brand, value->new ArrayList<>())
                    .addAll(lst);
        }
    );
});

Print the results

map.forEach((carType, brandMap) -> {
    System.out.println(carType);
    brandMap.forEach((brand, carList) -> {
        System.out.println("     " + brand + " -> " + carList);
    });
});

Prints

FAST
     AUDI -> [{FAST,  S3,  yellow}, {FAST,  R8,  silver}]
     BMW -> [{FAST,  Z4,  silver}, {FAST,  M3,  red}]
SLOW
     AUDI -> [{SLOW,  A1,  white}]
     BMW -> [{SLOW,  X1,  black}]

Note: the values between {} are the toString override of the Car class.

WJS
  • 36,363
  • 4
  • 24
  • 39
  • hello and big thanks for your time on this! I'm now testing the answers and so far, I really like this one. A friend of mine also suggested a `foreach` for the initial "work" as it would make things a bit easier to the eyes and for eventual maintenance, but I kept trying without it... I'll check the `computeIfAbsent` docs as this was totally skipped while trying to achieve a solution. I also updated the question with a "dirty" solution, have a look (and a laugh). – dNurb Sep 11 '20 at 17:44
1

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()))));
Naman
  • 27,789
  • 26
  • 218
  • 353
  • hey! Thanks for your time on this. I'm still doing some checks on both answers... and guess what I really like this one too! I was aiming for something like this (without `for/foreach`) but after seeing WJS's answer I'm confused... in a good way. Both deliver the desired results (also updated the question with a "bad" solution), and this is built the way I wanted to. I'm now debating with myself about the `Collectors` and `groupingBy` nesting... what's your opinion on this vs the other answer? I'm genuinely divided now. – dNurb Sep 11 '20 at 18:23
  • 1
    The solution in the question does a nested computation, on the other hand, both the iterate the initial `Map`, only once. There is not much of a difference I could figure out between the two solutions apart from a temporary `Map` created in the other answer, which is what typically a nested grouping internally could do as well. So I would have chosen either by readability. – Naman Sep 12 '20 at 03:22
  • 1
    Naman's solution is more in line with what you asked for. Had I remembered about the `new AbstractMap.SimpleEntry<>()` constructor I would have probably leaned more towards a similar solution. – WJS Sep 12 '20 at 13:35
  • Got it! Thank you both @Naman and @WJS for solutions and extra comments! I'll set this as the answer, but I really appreciate both. `computeIfAbsent` and `SimpleEntry` are not my daily cup of tea so it was nice to have them here. – dNurb Sep 15 '20 at 23:29