首页 > 解决方案 > Combine multiple `Collectors::groupBy` functions with Java Streams

问题描述

I have a problem with correctly combining multiple Collectors::groupingBy functions and then applying them all at once to a given input.

Let's say I have some class implementing following interface:

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

And now I can have some list of combinations of the methods from this interface, i.e. these lists can be: [Something::get1, Something::get3], [Something::get2, Something::get1, Something::get3].

Now, having such a list of methods and having a list of somethings, I would like to group those somethings by getters.

What I mean is that for example for the list [Something::get1, Something::get3] and a list [Something1, Something2, ...] I want to get the list of somethings grouped firstly by get1 and then by get2.

This can be achieved this way:

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

What if I have any arbitrary list of methods that I would like to apply to grouping?

I was thinking of something like this (ofc. this does not work, but you will get the idea):

Assume that List<Function<Something, String>> groupingFunctions is our list of methods we want to apply to grouping.

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

and then

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

But this approach does not work. How to achieve the result I am thinking of?

标签: javajava-streamcollectors

解决方案


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

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…</p>

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

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


推荐阅读