如何在Java Streams中组合多个Collectors::groupingBy函数?
Ah, I get it, dynamic nested grouping with Collectors.groupingBy can be tricky when you don't know the number of grouping keys upfront—great question! The issue with using reduce directly usually comes down to mismatched collector types or not starting with the right base collector. Let me break down how to fix this.
Why Your Initial reduce Attempt Might Have Failed
When you try to chain groupingBy with reduce, the key problem is that each subsequent groupingBy wraps the previous collector as its downstream. The base case (initial value) needs to be a terminal collector (like toList() or counting()), not another groupingBy. If you started with the wrong initial value, or didn't handle type erasure properly, the reduce operation would fail to compile or behave unexpectedly.
The Solution: Build Collectors From the Inside Out
Instead of trying to reduce forward, we'll build our nested collector from the innermost layer outward. Here's how it works:
- Start with your terminal collector (e.g.,
Collectors.toList()to collect items in each final group). - Iterate over your list of getters in reverse order, wrapping the current collector with
groupingByusing each getter as the key function.
This way, each step adds a new grouping level on top of the previous one, exactly like writing nested groupingBy calls manually.
Full Code Example
First, let's define your Something interface and a concrete implementation for testing:
interface Something { String get1(); String get2(); String get3(); String get4(); } // Test implementation class SomethingImpl implements Something { private final String g1, g2, g3, g4; public SomethingImpl(String g1, String g2, String g3, String g4) { this.g1 = g1; this.g2 = g2; this.g3 = g3; this.g4 = g4; } @Override public String get1() { return g1; } @Override public String get2() { return g2; } @Override public String get3() { return g3; } @Override public String get4() { return g4; } }
Now, here's a reusable method to create the dynamic nested collector:
import java.util.List; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; public class DynamicGrouping { // Generic method for dynamic nested grouping public static <T, R> Collector<T, ?, ?> nestedGrouping( List<Function<T, ?>> groupingFunctions, Collector<T, ?, R> terminalCollector ) { Collector<T, ?, ?> currentCollector = terminalCollector; // Iterate from last to first getter to build nested grouping for (int i = groupingFunctions.size() - 1; i >= 0; i--) { Function<T, ?> keyFunc = groupingFunctions.get(i); currentCollector = Collectors.groupingBy(keyFunc, currentCollector); } return currentCollector; } // Overload for when you just want to collect to a List public static <T> Collector<T, ?, ?> nestedGrouping(List<Function<T, ?>> groupingFunctions) { return nestedGrouping(groupingFunctions, Collectors.toList()); } public static void main(String[] args) { // Sample data List<Something> items = List.of( new SomethingImpl("A", "X", "P", "1"), new SomethingImpl("A", "Y", "P", "2"), new SomethingImpl("B", "X", "Q", "3"), new SomethingImpl("B", "X", "P", "4") ); // Example 1: Group by get1 → get3 List<Function<Something, String>> getters1 = List.of(Something::get1, Something::get3); var result1 = items.stream().collect(nestedGrouping(getters1)); // Result type: Map<String, Map<String, List<Something>>> System.out.println("Grouped by get1 → get3:"); result1.forEach((g1Val, innerMap) -> { System.out.printf(" %s:%n", g1Val); innerMap.forEach((g3Val, itemsList) -> System.out.printf(" %s: %d items%n", g3Val, itemsList.size()) ); }); // Example 2: Group by get2 → get1 → get3, counting items instead of listing List<Function<Something, String>> getters2 = List.of(Something::get2, Something::get1, Something::get3); var result2 = items.stream().collect(nestedGrouping(getters2, Collectors.counting())); // Result type: Map<String, Map<String, Map<String, Long>>> System.out.println("\nGrouped by get2 → get1 → get3 (counted):"); result2.forEach((g2Val, innerMap1) -> { System.out.printf(" %s:%n", g2Val); innerMap1.forEach((g1Val, innerMap2) -> { System.out.printf(" %s:%n", g1Val); innerMap2.forEach((g3Val, count) -> System.out.printf(" %s: %d%n", g3Val, count) ); }); }); } }
How It Works
- For
getters1 = [get1, get3], the method buildsgroupingBy(get1, groupingBy(get3, toList()))—exactly what you'd write manually. - For
getters2 = [get2, get1, get3], it createsgroupingBy(get2, groupingBy(get1, groupingBy(get3, counting()))). - The generic method works with any type of grouping function and terminal collector, making it flexible for different use cases.
Alternative: Using reduce (If You Prefer)
If you really want to use reduce instead of a loop, you can make it work by starting with the terminal collector and using a compatible accumulator. Note that parallel streams won't work here (hence the UnsupportedOperationException in the combiner):
Collector<Something, ?, ?> collector = getters.stream() .reduce( (Collector<Something, ?, ?>) Collectors.toList(), (acc, getter) -> Collectors.groupingBy(getter, acc), (a, b) -> { throw new UnsupportedOperationException("Parallel streams not supported for nested grouping"); } );
The loop approach is generally more readable and avoids potential type issues, though.
内容的提问来源于stack exchange,提问作者Mateusz

