简体   繁体   中英

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, ie 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?

You can do this:

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

This give you a raw collector, hence your stream result after grouping is also raw.

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

You can use this collector like this:

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

Because you know how many groupingBy you passed to the collector, you can cast it to appropriate Map result. In this case two groupingBy is applied:

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

Since you don't know how many functions are in the list, you can't declare a compile-time type reflecting the nesting. But even when using a collector type producing some unknown result type, composing it is not solvable in the clean functional way you intend. The closest you can get is

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

which has two fundamental problems. There is no way to provide a valid merge function for two Collector instances, so while this may work with a sequential operation, it is not a clean solution. Further, the nesting of the result maps will be in the opposite order; the last function of the list will provide the keys to the outermost map.

There might be ways to fix that, but all of them will make the code even more complicated. Compare this with a straight-forward loop:

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

You can use the collector like

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

but need instanceof and type casts to process the Map s…

It would be cleaner to create a single, non-nested Map with List keys which reflect the grouping functions:

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

It would allow querying entries like map.get(List.of(arguments, matching, grouping, functions))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM