Java Stream按多字段分组并实现多字段求和、求平均的方法
Java Stream 多字段分组与聚合操作问题解答
嘿,针对你提出的两个Java Stream分组聚合问题,我来一步步给你解答:
需求1:按name和code分组,统计account总数与time总和
要实现多字段分组+多维度聚合,核心要解决两个问题:构造复合分组键,以及同时计算多个聚合值。
解法1:Java 8兼容写法(无需额外依赖/语法特性)
首先,我们可以用Java自带的AbstractMap.SimpleEntry来做复合键(不用自定义类),再写一个简单的聚合结果类来存储统计值:
// 自定义聚合结果类,用来存account总数和time总和 class FooAggregateResult { private int totalAccount; private int totalTime; public FooAggregateResult(int totalAccount, int totalTime) { this.totalAccount = totalAccount; this.totalTime = totalTime; } // 累加单个Foo对象的数据 public FooAggregateResult add(Foo foo) { this.totalAccount += foo.getAccount(); this.totalTime += foo.getTime(); return this; } // 合并两个聚合结果(支持并行流场景) public FooAggregateResult merge(FooAggregateResult other) { this.totalAccount += other.totalAccount; this.totalTime += other.totalTime; return this; } // Getters和toString方便查看结果 public int getTotalAccount() { return totalAccount; } public int getTotalTime() { return totalTime; } @Override public String toString() { return "FooAggregateResult{totalAccount=" + totalAccount + ", totalTime=" + totalTime + "}"; } }
然后用Stream完成分组和聚合:
import java.util.AbstractMap; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; // 执行分组聚合 Map<AbstractMap.SimpleEntry<String, Integer>, FooAggregateResult> resultMap = fooList.stream() .collect(Collectors.groupingBy( // 复合分组键:把name和code打包成一个Entry foo -> new AbstractMap.SimpleEntry<>(foo.getName(), foo.getCode()), // 自定义收集器,实现累加与合并 Collectors.collectingAndThen( Collectors.reducing( new FooAggregateResult(0, 0), FooAggregateResult::add, FooAggregateResult::merge ), Function.identity() ) ));
解法2:Java 16+简洁写法(用Record简化代码)
如果你的项目已经用Java 16+,可以用Record来替代自定义类,代码会清爽很多:
// 用Record定义复合分组键 record FooGroupKey(String name, int code) {} // 用Record定义聚合结果 record FooStats(int totalAccount, int totalTime) {} // 分组聚合一行搞定 Map<FooGroupKey, FooStats> resultMap = fooList.stream() .collect(Collectors.groupingBy( foo -> new FooGroupKey(foo.getName(), foo.getCode()), // 用Collectors.teeing同时执行两个聚合,再合并结果 Collectors.teeing( Collectors.summingInt(Foo::getAccount), Collectors.summingInt(Foo::getTime), FooStats::new ) ));
这里的teeing是Java 12引入的收集器,专门用来同时执行两个聚合逻辑,然后把结果合并,非常适合这种多维度统计的场景。
需求2:能否同时用summingInt(Foo::getAccount)和averagingInt(Foo::getTime)实现需求?
完全可以,甚至还有更高效的方式,分两种情况说:
情况1:account字段值恒为1(如你的示例)
这时候分组内的元素数量就等于account的总和,用Collectors.summarizingInt(Foo::getTime)就能一次性拿到count(对应totalAccount)、sum、average等所有统计值,比单独用两个收集器更高效:
Map<FooGroupKey, IntSummaryStatistics> resultMap = fooList.stream() .collect(Collectors.groupingBy( foo -> new FooGroupKey(foo.getName(), foo.getCode()), Collectors.summarizingInt(Foo::getTime) )); // 后续获取结果: // resultMap.get(key).getCount() → account总数(元素数量) // resultMap.get(key).getAverage() → time的平均值
情况2:account字段值可能不为1
如果account的值可能变化,需要单独统计总和,就用teeing收集器同时执行两个聚合:
Map<FooGroupKey, Map<String, Object>> resultMap = fooList.stream() .collect(Collectors.groupingBy( foo -> new FooGroupKey(foo.getName(), foo.getCode()), Collectors.teeing( Collectors.summingInt(Foo::getAccount), Collectors.averagingInt(Foo::getTime), // 把两个聚合结果打包成Map返回 (totalAccount, avgTime) -> Map.of( "totalAccount", totalAccount, "averageTime", avgTime ) ) ));
内容的提问来源于stack exchange,提问作者San.s
相关产品推荐
相关产品推荐

