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

huangapple go评论84阅读模式
英文:

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的正确方法。我在其他简单的地图/列表情况下已经尝试过,但这个案例对我来说更加棘手。

尝试

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

mappedCars = allCarsAndBrands.entrySet()
                             .stream()
                             .collect(
                               groupingBy(Car::getType, 
                                 groupingBy(entry -> entry.getKey().getType())
                               )
                             );

我还遇到了“非静态无法引用非静态”错误(Map.Entry::getKey),但这是因为我未能匹配实际的预期返回(https://stackoverflow.com/questions/40172551/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,
}

编辑:“脏”解决方案

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

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.equals(car.brandName)).findFirst().get(),
                                Collectors.toList()
                        )
                ));

正如评论中提到的(在此之前应该已经添加了),这里存在一个“业务限制”为解决方案添加了一些限制。在现实世界中,我也不想创建一个新的CarBrand,因为这并不像在这个示例中看起来那么简单... 但是再次强调,使用原始地图并进行过滤+查找是不好的解决方案。

英文:

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

Map&lt;CarBrand, List&lt;Car&gt;&gt; allCarsAndBrands = new HashMap();

final String bmwBrandName = &quot;BMW&quot;;
final String audiBrandName = &quot;AUDI&quot;;

List&lt;Car&gt; bmwCars = new ArrayList();
bmwCars.add(new Car(CarType.FAST, &quot;Z4&quot;, &quot;silver&quot;, bmwBrandName));
bmwCars.add(new Car(CarType.FAST, &quot;M3&quot;, &quot;red&quot;, bmwBrandName));
bmwCars.add(new Car(CarType.SLOW, &quot;X1&quot;, &quot;black&quot;, bmwBrandName));

List&lt;Car&gt; audiCars = new ArrayList();
audiCars.add(new Car(CarType.FAST, &quot;S3&quot;, &quot;yellow&quot;, audiBrandName));
audiCars.add(new Car(CarType.FAST, &quot;R8&quot;, &quot;silver&quot;, audiBrandName));
audiCars.add(new Car(CarType.SLOW, &quot;A1&quot;, &quot;white&quot;, audiBrandName));

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

Map&lt;CarType, Map&lt;CarBrand, List&lt;Car&gt;&gt;&gt; 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 (https://stackoverflow.com/questions/40172551/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&lt;CarType, Map&lt;CarBrand, List&lt;Car&gt;&gt;&gt; mappedCars = allCarsAndBrands
                .values()
                .stream()
                .flatMap(List::stream)
                .collect(Collectors.groupingBy(
                        Car::getType,
                        Collectors.groupingBy(
                                car -&gt; allCarsAndBrands.keySet().stream().filter(brand -&gt; 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.

答案1

得分: 1

如评论中所讨论的那样,如果将 Make 包含为 Car 类的一个字段,那么这个操作就很简单。

根据您最后的评论,最简单的方法是使用 Java 8 中引入的 StreamMap 接口的其他特性来实现混合解决方案。

数据

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

注意:{} 中的值是 Car 类的 toString 重写结果。

英文:

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&lt;CarBrand, List&lt;Car&gt;&gt; allCarsAndBrands = new HashMap&lt;&gt;();

final String bmwBrandName = &quot;BMW&quot;;
final String audiBrandName = &quot;AUDI&quot;;

List&lt;Car&gt; bmwCars = new ArrayList&lt;&gt;();
bmwCars.add(new Car(CarType.FAST, &quot;Z4&quot;, &quot;silver&quot;));
bmwCars.add(new Car(CarType.FAST, &quot;M3&quot;, &quot;red&quot;));
bmwCars.add(new Car(CarType.SLOW, &quot;X1&quot;, &quot;black&quot;));

List&lt;Car&gt; audiCars = new ArrayList&lt;&gt;();
audiCars.add(new Car(CarType.FAST, &quot;S3&quot;, &quot;yellow&quot;));
audiCars.add(new Car(CarType.FAST, &quot;R8&quot;, &quot;silver&quot;));
audiCars.add(new Car(CarType.SLOW, &quot;A1&quot;, &quot;white&quot;));

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

Process

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

Map&lt;CarType, Map&lt;CarBrand, List&lt;Car&gt;&gt;&gt; map = new HashMap&lt;&gt;();

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

Print the results

map.forEach((carType, brandMap) -&gt; {
	System.out.println(carType);
	brandMap.forEach((brand, carList) -&gt; {
		System.out.println(&quot;     &quot; + brand + &quot; -&gt; &quot; + carList);
	});
});

Prints

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

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

答案2

得分: 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()))));
英文:

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

huangapple
  • 本文由 发表于 2020年9月11日 05:02:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/63837641.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定