Java Collectors.groupingBy 进阶:多级分组、下游收集器与统计聚合一次搞定

Java Collectors.groupingBy 进阶:多级分组、下游收集器与统计聚合一次搞定

「把订单按城市分组」------Collectors.groupingBy 一行就能做。但真实需求往往是:「按城市分组,每组再按月份分组,统计每个月的总金额」。这时候如果你还在 groupingBy 之后套一堆 forEach 手动累加,代码就会迅速失控。

groupingBy 的威力全在它的第二个、第三个参数------下游收集器(downstream collector)。搞懂它,一次流式操作就能做完多级分组 + 聚合统计,不用中间变量,也不用手写循环。这篇把最常用也最容易踩坑的几种组合讲透。

准备数据

java 复制代码
record Order(String city, String month, String product, long amount) {}

List<Order> orders = List.of(
    new Order("北京", "01", "手机", 3000),
    new Order("北京", "01", "耳机", 500),
    new Order("北京", "02", "手机", 4000),
    new Order("上海", "01", "手机", 3500),
    new Order("上海", "02", "耳机", 800)
);

最朴素的分组:值是 List

只传一个参数时,groupingBy 把每组的元素收进 List:

java 复制代码
Map<String, List<Order>> byCity = orders.stream()
    .collect(Collectors.groupingBy(Order::city));
// {北京=[3个订单], 上海=[2个订单]}

这只是起点。大多数时候我们不想要「一堆原始对象的 List」,而是想要每组的某个统计值。这就要用到下游收集器。

关键一步:第二个参数控制「每组收成什么」

groupingBy(分类函数, 下游收集器)------第二个参数决定每个分组的 value 长什么样。

每组求和 ,用 Collectors.summingLong:

java 复制代码
Map<String, Long> amountByCity = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.summingLong(Order::amount)));
// {北京=7500, 上海=4300}

每组计数 ,用 Collectors.counting:

java 复制代码
Map<String, Long> countByCity = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.counting()));
// {北京=3, 上海=2}

每组一次性拿全套统计 (总数、和、最大、最小、平均),用 summarizingLong:

java 复制代码
Map<String, LongSummaryStatistics> stats = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.summarizingLong(Order::amount)));
System.out.println(stats.get("北京").getMax());     // 4000
System.out.println(stats.get("北京").getAverage()); // 2500.0

一个 summarizingLong 就省掉了你分别写 sum、count、max、average 四段代码。

多级分组:下游收集器再套一个 groupingBy

下游收集器本身可以又是一个 groupingBy------这就是多级分组的实现方式。按城市分组,每组内部再按月份分组求和:

java 复制代码
Map<String, Map<String, Long>> byCityThenMonth = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.groupingBy(
            Order::month,
            Collectors.summingLong(Order::amount))));
// {北京={01=3500, 02=4000}, 上海={01=3500, 02=800}}

嵌套几层就是几级分组。这段代码如果用传统循环写,至少要两层嵌套 MapcomputeIfAbsent 加手动累加,可读性和这个没法比。

mapping:分组后先转换再收集

有时你不想收集整个对象,只想收集它的某个字段。Collectors.mapping 在收进容器之前先做一次映射:

java 复制代码
// 每个城市卖过哪些产品(去重)
Map<String, Set<String>> productsByCity = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.mapping(Order::product, Collectors.toSet())));
// {北京=[手机, 耳机], 上海=[手机, 耳机]}

mapping 的第一个参数是转换函数,第二个参数才是真正的收集器。它常和 toList/toSet/joining 搭配。

一个高频坑:groupingBy 的 value 默认是可变的,但别依赖它

groupingBy 默认用 HashMap 装结果,value 的 List 默认是 ArrayList。如果你需要有序的分组结果(比如按城市名排序输出),要用三参重载显式指定 Map 类型:

java 复制代码
Map<String, Long> sortedByCity = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        TreeMap::new,                         // 第二个参数指定 Map 实现,结果按 key 排序
        Collectors.summingLong(Order::amount)));
// TreeMap: {上海=4300, 北京=7500}

注意参数顺序:三参版是 (分类函数, Map 工厂, 下游收集器),别把 Map 工厂和下游收集器写反了------写反会直接编译不过,但新手常在这卡壳。

filtering:Java 9+ 先过滤再分组,空组也保留

一个容易踩的坑:如果你在 stream() 上先 filtergroupingBy,被过滤掉的组会整个消失 。而 Collectors.filtering(Java 9 引入)是在组内过滤,能保留空组:

java 复制代码
// 只统计金额 >= 1000 的订单,但所有城市都要出现在结果里
Map<String, List<Order>> big = orders.stream()
    .collect(Collectors.groupingBy(
        Order::city,
        Collectors.filtering(o -> o.amount() >= 1000, Collectors.toList())));
// 若某城市全是小额订单,它对应的 value 会是空 List,而不是从 Map 里消失

到底要不要保留空组,取决于业务。要做报表「每个城市都要有一行」,就用 filtering;只关心有数据的组,stream().filter() 更省。

小结

  • groupingBy 的精髓在下游收集器 :第二个参数决定「每组收成什么」,别只会返回 List 然后手动循环。
  • 求和 summingLong、计数 counting、全套统计 summarizingLong------分组即聚合,一步到位。
  • 多级分组 就是把下游收集器再写成一个 groupingBy,嵌套几层就是几级。
  • mapping 在收集前转换字段;filtering(Java 9+)在组内过滤且保留空组,和 stream().filter() 语义不同。
  • 需要结果有序用三参重载传 TreeMap::new,注意参数顺序是 (分类, Map 工厂, 下游)

一句话记忆:groupingBy 的第二个参数才是主角,把「怎么分组」和「每组算什么」拆成两件事,组合起来就无所不能。

相关推荐
空 白II1 小时前
cuda toolkit 10.0 windows 安装实录
windows
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之前端按钮级安全与路由守卫
java·安全·spring
ocean21031 小时前
2025-2026年Java语言及生态面试高频知识点洞察
java·开发语言·面试
2601_962078231 小时前
Python接口自动化流程(pytest)
python·pytest·接口自动化·测试框架·断言
2302_1111 小时前
java时间类型
java·开发语言
用户8181870627461 小时前
第25章 Redis集群脑裂实录:一次真实故障复盘
java·后端
运行时异常1 小时前
【WMS 仓储系统集成 AI Agent 实战】第 6 讲:RAG 知识库——“输出简单流程“为什么检索不到任何东西
java
小七在进步1 小时前
类和对象(三)
java·开发语言