【问题标题】:Grouping a map by object's property to a new Map按对象的属性将地图分组到新地图
【发布时间】:2020-09-10 21:02:25
【问题描述】:

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

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 和相关汽车的地图结构。

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

尝试

这是一个初始代码“尝试”:

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

我也收到“非静态无法引用错误”(Map.Entry::getKey),但这是因为我未能匹配实际的预期回报(Static context cannot access non-static in Collectors

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

附加内容

以下是此示例的类定义:

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”解决方案(基于 cmets 的建议,将检查答案!):

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

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

【问题讨论】:

  • A Car 知道它的“型号”但不知道它的“品牌”(“品牌”)?你应该先解决这个问题。
  • 现在您在Car 对象中有品牌,这很容易:allCarsAndBrands.values().stream().flatMap(List::stream).collect(Collectors.groupingBy(Car::getType, Collectors.groupingBy(Car::getBrand, Collectors.toList())))
  • 我不明白:你想要所有 FAST 和 SLOW 车,而不关心品牌,不是吗? Stream.concat(bmwCars, audiCars).collect(groupingBy(Car::getCarType)) 够吗?
  • 当然,如果你不想把它放在它所属的Car 中,创建一个Map&lt;String, CarBrand&gt; 并查找它。我没有看到问题。

标签: java java-8 java-stream method-reference collect


【解决方案1】:

通过使用现有模型以及嵌套分组的初始方法,您的思路是正确的。可以通过考虑在迭代条目时扁平化 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()))));

【讨论】:

  • 嘿!感谢您在这方面的时间。我仍在对这两个答案进行一些检查……猜猜我也真正喜欢这个答案!我的目标是这样的(没有for/foreach),但在看到WJS的答案后,我很困惑......以一种好的方式。两者都提供了预期的结果(也用“坏”的解决方案更新了问题),这是按照我想要的方式构建的。我现在正在与自己辩论 CollectorsgroupingBy 嵌套......您对此与其他答案有何看法?我现在真的分裂了。
  • 问题中的解决方案执行嵌套计算,另一方面,都只迭代初始 Map 一次。除了在另一个答案中创建的临时Map 之外,我可以弄清楚这两种解决方案之间没有太大区别,这通常是内部嵌套分组也可以做到的。所以我会选择可读性。
  • 纳曼的解决方案更符合您的要求。如果我记得 new AbstractMap.SimpleEntry&lt;&gt;() 构造函数,我可能会更倾向于类似的解决方案。
  • 知道了!感谢@Naman 和@WJS 的解决方案和额外的cmets!我会将此作为答案,但我真的很感激。 computeIfAbsentSimpleEntry 不是我每天喝的一杯茶,所以很高兴有它们在这里。
【解决方案2】:

正如 cmets 中所讨论的,如果将 Make 包含为 Car 类的字段,这很容易做到。

根据您的上一条评论,最简单的方法是使用混合解决方案,使用 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&lt;CarType, List&lt;Car&gt;&gt; 然后反转密钥来实现的。您可能不熟悉的唯一新功能是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}]

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

【讨论】:

  • 您好,非常感谢您抽出宝贵时间!我现在正在测试答案,到目前为止,我真的很喜欢这个答案。我的一个朋友还建议使用foreach 来进行最初的“工作”,因为它会使事情变得更容易看和最终的维护,但我一直在尝试不使用它......我会检查computeIfAbsent 文档因为在尝试实现解决方案时完全跳过了这一点。我还用“肮脏”的解决方案更新了这个问题,看看(笑)。
猜你喜欢
  • 2023-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多