Java 8:按字段对集合进行分组,并使用流将集合展平并作为映射值加入集合?

Java 8: grouping a collection by a field and flatten and join a collection as mapped value using stream?

我的 class 有两个字段:

我有一个此类对象的列表,我想要的是获得一个 Map<MyKey, Set<MyEnum>,其中的值是使用此键从对象的所有 myEnum 中加入的。

例如,如果我有三个对象:

  1. myKey: key1, myEnums: [E1]
  2. myKey: key1, myEnums: [E2]
  3. myKey: key2, myEnums: [E1, E3]

预期结果应该是:

key1 => [E1, E2], key2 => [E1, E3]

我想出了这个代码:

Map<MyKey, Set<MyEnum>> map = myObjs.stream()
        .collect(Collectors.groupingBy(
                MyType::getMyKey,
                Collectors.reducing(
                        new HashSet<MyEnum>(),
                        MyType::getMyEnums,
                        (a, b) -> {
                            a.addAll(b);
                            return a;
                        })));

它有两个问题:

  1. reducing 中的 HashSet 似乎在所有键之间共享。也就是说,上述示例的实际 运行 结果是 key1 => [E1, E2, E3], key2 => [E1, E2, E3]。为什么会这样?

  2. 即使此代码有效,它看起来也很难看,尤其是在减少部分,我必须手动处理构建连接集合的逻辑。有更好的方法吗?

谢谢!

请注意,您只创建了 一个 身份对象:new HashSet<MyEnum>().

您作为第三个参数提供的 BinaryOperator 必须是 idempotent,与常见的数学运算符相同,例如x = y + z 不会更改 yz 的值。

这意味着您需要合并两个输入集ab,而不更新任何一个。

此外,使用枚举时,您应该使用 EnumSet,而不是 HashSet

Map<MyKey, Set<MyEnum>> map = myObjs.stream()
        .collect(Collectors.groupingBy(
                    MyType::getMyKey,
                    Collectors.reducing(
                        EnumSet.noneOf(MyEnum.class), // <-- EnumSet
                        MyType::getMyEnums,
                        (a, b) -> {
                            EnumSet<MyEnum> c = EnumSet.copyOf(a); // <-- copy
                            c.addAll(b);
                            return c;
                        })));

更新

更短、更精简的版本,在累积结果时不必继续创建新集:

Map<MyKey, Set<MyEnum>> map = myObjs.stream()
        .collect(Collectors.groupingBy(
                    MyType::getMyKey,
                    Collector.of(
                            () -> EnumSet.noneOf(MyEnum.class),
                            (r, myObj) -> r.addAll(myObj.getMyEnums()),
                            (r1, r2) -> { r1.addAll(r2); return r1; }
                    )));

不理想,但使用可变容器使其相当容易理解。

myObjs.stream()
  .collect(groupingBy(MyType::getMyKey)
  .entrySet().stream()
  .collect(toMap(
    Map.Entry::getKey, 
    e -> e.getValue()
      .stream()
      .flatMap(v -> v.getMyEnums().stream())
      .collect(toSet())
  )

Collectors.mapping(Function, Collector) 非常适合您想在这里做的事情,如果只是 Collectors.flatMapping

编辑:在 java 9 出来之前, 中有一个方便的 flatMapping 实现。有了它,我们的解决方案如下所示:

myObjs.stream()
  .collect(
    groupingBy(MyType::getMyKey,
    flatMapping(v -> v.getMyEnums().stream(), toSet())
  );