You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Java Streams中组合多个Collectors::groupingBy函数?

Dynamic Nested Grouping with Arbitrary Getters in Java

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:

  1. Start with your terminal collector (e.g., Collectors.toList() to collect items in each final group).
  2. Iterate over your list of getters in reverse order, wrapping the current collector with groupingBy using 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 builds groupingBy(get1, groupingBy(get3, toList()))—exactly what you'd write manually.
  • For getters2 = [get2, get1, get3], it creates groupingBy(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:39:28