Java Collectors.teeing 汇总两条统计管道
来源:17golang原创
时间:2026-10-10 18:24:50 385浏览 收藏
我第一次把订单列表同时汇总成“订单数”和“平均金额”时,直觉写法是连续调用两次 stream()。代码能工作,但业务结果分散在两个表达式里,后面再增加最大金额或状态计数时,很快就会变成一串相互独立的遍历。Java 的 Collectors.teeing 适合处理这个问题:同一批元素同时交给两个下游 Collector,最后由一个 merger 把两个结果组合成一个对象。
官方地址:https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/stream/Collectors.html
这篇只解决一个边界明确的任务:在一次 collect 中组合两条统计管道。示例使用 Java 12 及以上提供的 Collectors.teeing,不把它包装成所有 Stream 问题的通用答案。
这个场景为什么适合 teeing
如果一批订单已经在一个 Stream 中,想同时得到数量与平均金额,目标结果可以先抽象成:
OrderSummary(totalCount, averageAmount)
传统写法通常是先收集数量、再收集金额统计;而 teeing 把“两个独立的归约结果”和“最终业务对象”放在同一次收集中表达。Oracle API 对它的定义就是一个由两个下游 Collector 组成的复合 Collector:每个输入元素都会被两个下游分别处理,两个结果完成后再交给合并函数。
这里有一个容易忽略的取舍:teeing 是组合收集器,不是让两个统计天然共享所有中间状态。它适合两个结果都能由各自 Collector 清晰表达的场景;如果两条管道有复杂的共享状态或需要严格控制遍历顺序,显式循环反而更容易维护。
先把两条统计管道拆开
先不急着写完整业务对象,分别看两个下游 Collector。第一条数订单,第二条从订单金额生成摘要统计。这样做的好处是可以先确认每条管道的结果类型,再处理合并函数的泛型。

record Order(String id, double amount) {}
List orders = List.of(
new Order("A-100", 18.5),
new Order("A-101", 42.0),
new Order("A-102", 25.5)
);
var count = orders.stream()
// 第一条管道只关心元素数量
.collect(Collectors.counting());
var amountStats = orders.stream()
// 第二条管道把金额映射为 double 后汇总
.collect(Collectors.summarizingDouble(Order::amount));
System.out.println(count); // 输出 3
System.out.println(amountStats.getAverage()); // 输出 28.666666666666668
两次单独收集很直观,却也说明了重复遍历的来源。进入 teeing 后,counting() 的结果类型是 Long,summarizingDouble() 的结果类型是 DoubleSummaryStatistics;合并器必须接住这两个具体结果。
用 merger 组装结果对象
接下来把两个下游 Collector 放进同一个 collect。merger 的两个参数分别对应第一个和第二个下游结果,返回值就是整个 collect 的结果。用 record 承载最终结果,可以避免返回一个难以理解的 Map 或数组。

import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
record Order(String id, double amount) {}
record OrderSummary(long totalCount, double averageAmount) {}
List orders = List.of(
new Order("A-100", 18.5),
new Order("A-101", 42.0),
new Order("A-102", 25.5)
);
OrderSummary summary = orders.stream()
.collect(Collectors.teeing(
// 下游一:计算订单数量
Collectors.counting(),
// 下游二:计算金额摘要
Collectors.summarizingDouble(Order::amount),
// 合并两个下游结果,形成领域对象
(count, stats) -> new OrderSummary(count, stats.getAverage())
));
System.out.println(summary.totalCount());
System.out.println(summary.averageAmount());
这个例子最值得记住的不是语法,而是类型关系:teeing(first, second, merger) 的 merger 接收 R1 与 R2,输出 R。因此把复杂逻辑塞进两个下游 Collector,把对象组装留在最后一步,代码的职责边界会比较稳定。
把一次 collect 放进完整工作流
实际项目里,我会按“确定结果对象—选择两个下游—处理空数据—决定是否并行”的顺序落地,而不是一看到两个统计字段就直接嵌套 teeing。
- 先确定结果对象。 明确字段的业务含义和精度,例如平均金额是否保留原始 double,还是在合并器中转成
BigDecimal。 - 再选择下游 Collector。 数量可以用
counting(),数值摘要可以用summarizingDouble(),分组后的统计也可以继续嵌套一个下游 Collector。 - 把空数据写进合并器。
DoubleSummaryStatistics#getAverage()在没有元素时返回 0.0;如果业务上需要“没有平均值”而不是“平均值为 0”,应把结果字段设计成OptionalDouble或显式状态。 - 最后才考虑并行。 两条 Collector 都必须正确支持组合器,合并器本身也应是无副作用的。不要仅因为写了
parallelStream()就假设结果更快;数据量、拆分成本和下游 Collector 的特性都要先确认。
record SafeSummary(long totalCount, Double averageAmount) {}
SafeSummary summary = orders.stream()
.collect(Collectors.teeing(
// 计数结果保留 long,避免把数量塞进 double
Collectors.counting(),
// 摘要对象负责识别空输入
Collectors.summarizingDouble(Order::amount),
(count, stats) -> new SafeSummary(
count,
// 空集合返回 null,和“平均金额正好为 0”区分开
stats.getCount() == 0 ? null : stats.getAverage()
)
));
如果两条统计管道都需要同一个复杂转换,先用 map 把输入整理成中间记录,再交给 teeing,通常比在两个下游里重复转换更清楚。
常见误区与速查
把 teeing 当成“自动提速”工具
它首先改善的是结果组合和代码表达,不应直接承诺性能提升。下游 Collector 仍然需要各自维护状态,复杂统计还可能增加内存开销。要比较性能,应使用相同数据、相同数据源和独立基准,而不是根据一次运行的感觉下结论。
忽略下游结果的真实类型
counting() 返回的是 Long,摘要 Collector 返回的是对应的统计对象;合并器参数名写得再短,也不能改变类型。遇到编译器推断困难时,先把合并器拆成具名方法,或为结果 record 明确字段类型。
为了三个以上结果无限嵌套
两个结果组合得很自然,超过两层后可读性会快速下降。此时可以先定义一个中间统计对象,或写一个自定义 Collector;如果逻辑还伴随校验、短路和错误收集,显式循环可能更合适。
| 需求 | 建议写法 | 注意点 |
|---|---|---|
| 同时得到两个独立汇总 | Collectors.teeing | merger 只负责组合最终结果 |
| 单个数值统计 | summing 或 summarizing | 不必为了统一形式套 teeing |
| 复杂共享状态 | 自定义 Collector 或显式循环 | 优先保证可读性和可测试性 |
| 空输入有业务语义 | 合并器显式转换状态 | 区分“没有值”和“值为零” |
回到最初的问题:Java Collectors.teeing 适合把同一批元素交给两条清晰、相互独立的统计管道,再将结果组装为一个业务对象。先设计结果类型,再选下游 Collector,最后写无副作用的 merger,通常就是最稳妥的工作流。
相关问题
- teeing 能不能嵌套? 可以,但超过两层后建议用中间结果对象提升可读性。
- 空集合的平均值是什么?
DoubleSummaryStatistics的平均值为 0.0;业务上需要缺失语义时应自行转换。 - teeing 适合并行流吗? 可以参与并行收集,但要确认两个下游 Collector 及合并逻辑都能安全组合,并用基准测试验证收益。
-
289 收藏
-
303 收藏
-
116 收藏
-
数据库 · Redis | 3个月前 | Redis · 消息队列 · Stream · redis 消息队列 Redis Stream 消费组 XREADGROUP XACK XPENDING XAUTOCLAIM187 收藏
-
148 收藏
-
189 收藏
-
385 收藏
-
165 收藏
-
文章 · java教程 | 6小时前 | 异常处理 · 并发编程 · Java教程 · StructuredTaskScope Java Structured Concurrency Joiner allUntil 子任务异常 聚合异常260 收藏
-
121 收藏
-
418 收藏
-
415 收藏
-
文章 · java教程 | 1天前 | spring · Spring Boot · 事务管理 · Java教程 · REQUIRES_NEW Spring事务事件 TransactionalEventListener AFTER_COMMIT Spring事件126 收藏
-
文章 · java教程 | 1天前 | 配置管理 · Java · Spring Boot · 故障排查 · Java教程 · spring boot actuator @ConfigurationProperties 配置属性绑定 PropertySource184 收藏
-
319 收藏
-
418 收藏
-
435 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习