按对象 属性 将地图分组到新地图

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

首先,让我添加实际的“示例代码”:

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;

问题

我的目标是用 CarType 填充 mappedCars,这将产生两个大集合:一个包含所有 FAST 汽车,另一个包含所有 SLOW 汽车(或任何未来的“类型” ,每个都具有以前的地图结构 CarBrand 和相关的汽车)。

我目前无法找到 Collections/Streams 的正确用法,用于此“在其他地图中包含列表的地图”。我遇到过其他简单 maps/lists 的案例,但事实证明这个案例对我来说更棘手。

尝试次数

这是“尝试”的初始代码:

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

我也收到“无法引用非静态错误”(Map.Entry::getKey),但这是因为我未能匹配实际预期 return ()

此时我只是感到困惑,也尝试使用 Collectors.toMap 但仍然无法获得工作分组。

额外内容

以下是此示例的 class 定义:

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,
}

编辑:“脏”解决方案

这是一个“hackish”的解决方案(基于评论建议,将检查答案!):

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()
                        )
                ));

如评论中所述(之前应该在此处添加),有一个“业务约束”为解决方案增加了一些限制。我也不想创建一个新的 CarBrand,因为在现实世界中,这并不像上面看到的那么简单……但同样,使用原始地图和过滤 + 查找是很糟糕的。

正如评论中所讨论的那样,如果将 Make 作为 Car class.

的一个字段,这很容易做到

根据您最后的评论,最简单的方法是使用 Stream 和 Java 8 中引入的 Map 界面的其他功能的混合解决方案。

数据

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);

进程

这通过为每个 CarBrand 创建一个 Map<CarType, List<Car>> 然后反转键来实现。您可能不熟悉的唯一新功能是 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);
        }
    );
});

打印结果

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

打印

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

注意:{} 之间的值是 toString 覆盖 Car class。

通过使用现有模型和嵌套分组的初始方法,您的思考方向是正确的。可以在迭代条目时考虑展平 Map 的值部分来进行改进。

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

一旦你有了它,分组概念的工作原理几乎相同,但现在默认返回的分组值将改为条目类型。因此还需要 mapping。这使得整体解决方案类似于:

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()))));