【问题标题】:Combine multiple `Collectors::groupBy` functions with Java Streams将多个 `Collectors::groupBy` 函数与 Java Streams 结合
【发布时间】:2018-11-06 21:35:24
【问题描述】:

我在正确组合多个 Collectors::groupingBy 函数然后将它们一次全部应用于给定输入时遇到问题。

假设我有一些类实现了以下接口:

interface Something {
    String get1();
    String get2();
    String get3();
    String get4();
}

现在我可以从这个接口获得一些方法组合的列表,即这些列表可以是: [Something::get1, Something::get3], [Something::get2, Something::get1, Something::get3].

现在,有了这样一个方法列表和一个东西列表,我想按 getter 对这些东西进行分组。

我的意思是,例如,对于列表 [Something::get1, Something::get3] 和列表 [Something1, Something2, ...],我希望首先按 get1 分组,然后按 get2 分组。

可以这样实现:

var lst = List.of(smth1, smth2, smth3);
lst.stream()
   .collect(Collectors.groupingBy(Something::get1, Collectors.groupingBy(Something::get3)))

如果我有任何想要应用于分组的任意方法列表怎么办?

我在想这样的事情(ofc。这不起作用,但你会明白的):

假设List<Function<Something, String>> groupingFunctions 是我们要应用于分组的方法列表。

var collector = groupingFunctions.stream()
                                 .reduce((f1, f2) -> Collectors.groupingBy(f1, Collectors.groupingBy(f2)))

然后

List.of(smth1, smth2, smth3).stream().collect(collector)

但是这种方法行不通。如何达到我想的结果?

【问题讨论】:

  • 最终结果如何?
  • 最终结果是嵌套地图。
  • 最终的Map键值类型是4嵌套的,可以raw type吗?
  • 如果你不知道它的类型,你会如何使用结果?
  • @Holger - 如果Something 有一个属性List<Something> children 和一个属性String name,我会将映射重新映射到Something 对象,其中键是名称,列表将是孩子,所以这不会有问题。我只是想有一个通用的解决方案来创建一个深度嵌套的树状结构。

标签: java java-stream collectors


【解决方案1】:

你可以这样做:

public static Collector createCollector(Function<A, String>... groupKeys) {
    Collector collector = Collectors.toList();
    for (int i = groupKeys.length - 1; i >= 0; i--) {
        collector = Collectors.groupingBy(groupKeys[i], collector);
    }
    return collector;
}

这为您提供了一个原始收集器,因此分组后的流结果也是原始的。

Collector collector = createCollector(Something::get1, Something::get2);

你可以像这样使用collector

Object result = somethingList.stream().collect(collector);

因为您知道有多少 groupingBy 传递给收集器,您可以将其转换为适当的 Map 结果。在这种情况下,应用了两个 groupingBy

Map<String, Map<String, List<Something>>> mapResult = (Map<String, Map<String, List<Something>>>) result

【讨论】:

  • 实际上,我无法转换它,因为我没有任何关于将应用多少 groupingBy 的先验知识。它可以是 1、2、5 或任何其他数量的分组。另外,我想坚持函数式方法,这就是我提到 reduce 的原因。我知道我可以遍历函数列表,但我不想这样做。
  • 您可以使用functional 方式重构createCollector。但没有什么可以解决cast 部分。最后你仍然需要知道Map的类型才能进行投射
【解决方案2】:

由于您不知道列表中有多少个函数,因此您无法声明反映嵌套的编译时类型。但是,即使使用产生某些未知结果类型的收集器类型,也无法以您想要的干净功能方式来组合它。你能得到的最接近的是

var collector = groupingFunctions.stream()
    .<Collector<Something,?,?>>reduce(
        Collectors.toList(),
        (c,f) -> Collectors.groupingBy(f, c),
        (c1,c2) -> { throw new UnsupportedOperationException("can't handle that"); });

这有两个基本问题。无法为两个 Collector 实例提供有效的合并功能,因此虽然这可能适用于顺序操作,但它不是一个干净的解决方案。此外,结果映射的嵌套顺序相反;列表的最后一个函数将提供最外层地图的键。

可能有一些方法可以解决这个问题,但所有这些都会使代码变得更加复杂。将此与直接循环进行比较:

Collector<Something,?,?> collector = Collectors.toList();
for(var i = groupingFunctions.listIterator(groupingFunctions.size()); i.hasPrevious(); )
    collector = Collectors.groupingBy(i.previous(), collector);

你可以像这样使用收集器

Object o = lst.stream().collect(collector);

但需要instanceof 和类型转换来处理Maps...

使用反映分组功能的List 键创建单个非嵌套Map 会更简洁:

Map<List<String>,List<Something>> map = lst.stream().collect(Collectors.groupingBy(
    o -> groupingFunctions.stream().map(f -> f.apply(o))
                          .collect(Collectors.toUnmodifiableList())));

它将允许查询像map.get(List.of(arguments, matching, grouping, functions))这样的条目

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-03
    • 2020-06-13
    • 1970-01-01
    • 2020-01-18
    • 1970-01-01
    • 2013-10-21
    • 2021-10-08
    • 2013-01-09
    相关资源
    最近更新 更多