Java Collectors.teeing 汇总两条统计管道
我第一次把订单列表同时汇总成“订单数”和“平均金额”时,直觉写法是连续调用两次 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 及合并逻辑都能安全组合,并用基准测试验证收益。
go fix modernizers 批量迁移旧标准库写法
- 上一篇
- go fix modernizers 批量迁移旧标准库写法
- 下一篇
- Python dataclass __post_init__ 计算派生字段
-
- 文章 · java教程 | 33分钟前 |
- Java Pattern Matching switch 处理密封层级
- 189浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- Java Foreign Function Memory API 管理本地内存生命周期
- 385浏览 收藏
-
- 文章 · java教程 | 3小时前 |
- Java StampedLock 乐观读校验失败的回退方式
- 165浏览 收藏
-
- 文章 · java教程 | 6小时前 | 异常处理 · 并发编程 · Java教程 · StructuredTaskScope Java Structured Concurrency Joiner allUntil 子任务异常 聚合异常
- Java Structured Concurrency 汇总子任务异常
- 260浏览 收藏
-
- 文章 · java教程 | 8小时前 |
- Java ForkJoinPool asyncMode 调整任务队列顺序
- 121浏览 收藏
-
- 文章 · java教程 | 18小时前 | 可观测性 · Java教程 · LongAdder JFR事件流 RecordingStream Java延迟指标 近似分位数
- JFR 事件流如何在线聚合延迟指标
- 418浏览 收藏
-
- 文章 · java教程 | 22小时前 | Java · 并发编程 · CompletableFuture · java completablefuture 取消原因 异步编排
- Java CompletableFuture 怎样在阶段之间保留取消原因
- 415浏览 收藏
-
- 文章 · java教程 | 1天前 | spring · Spring Boot · 事务管理 · Java教程 · REQUIRES_NEW Spring事务事件 TransactionalEventListener AFTER_COMMIT Spring事件
- Spring 事务事件监听器如何在提交后执行
- 126浏览 收藏
-
- 文章 · java教程 | 1天前 | 配置管理 · Java · Spring Boot · 故障排查 · Java教程 · spring boot actuator @ConfigurationProperties 配置属性绑定 PropertySource
- Spring Boot 配置属性绑定失败怎样定位字段来源
- 184浏览 收藏
-
- 文章 · java教程 | 1天前 | Java ·
- Java Vector API 怎样批量计算浮点数组
- 319浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · java Class-File API LineNumberTable
- Java Class-File API 如何重写方法的行号表属性
- 418浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · 性能优化 · Java教程 · arena 内存映射 超大文件 FileChannel.map Java MemorySegment
- Java 内存段怎样安全映射超大文件
- 435浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 408次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 483次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 493次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 438次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 266次使用
-
- golang HTTP 服务器 处理 日志/Stream流的操作
- 2023-01-07 289浏览
-
- Go语言中的函数式编程实践
- 2023-01-07 303浏览
-
- PHP 連結MySQL 常用函數
- 2023-02-23 116浏览
-
- Redis Stream 消息队列实战:消费组、ACK 和失败重投怎么配
- 2026-06-13 187浏览
-
- Redis XAUTOCLAIM 怎么接管积压消息:游标、最小空闲时间与重试边界
- 2026-08-11 148浏览

