collectors.teeing 是 java 12 引入的单次遍历双路聚合工具,避免重复流处理和不可重用流异常,通过两个独立 collector 和一个合并函数实现语义无关指标的高效提取。

Collectors.teeing 是 Java 12 引入的,用来在一个 Stream 上**同时做两路独立聚合**,再把结果合起来——不是“先分再合”的手动拆流,而是单次遍历、双路计算、一次收口。
为什么不能用两次 collect()?
常见误区是想调两次 collect():比如既要算平均值又要找最大值,就写两遍 stream.collect(...)。这会触发两次完整遍历,对中间操作(如 filter、map)重复执行,性能白丢;更糟的是,如果流源不可重用(比如 Files.lines() 或自定义无限流),第二次调用直接抛 IllegalStateException。
正确做法是用 teeing 保证只走一遍:
Map.Entry<Integer, Double> result = list.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue), // 第一路:求和
Collectors.averagingDouble(Integer::doubleValue), // 第二路:平均值
AbstractMap.SimpleEntry::new // 合并函数
));
Collectors.teeing 的三个参数怎么填?
它的签名是 teeing(Collector<t>, Collector<t>, BiFunction<r1>)</r1></t></t>,三个参数必须严格对应:
立即学习“Java免费学习笔记(深入)”;
- 前两个是任意合法
Collector,类型可以不同(比如一个返回Integer,一个返回String),但输入类型T必须一致(都得能处理流里的元素) - 第三个是合并函数,接收前两路的结果(
R1和R2),返回最终结果R;别写成(a, b) -> a + b这种硬拼,类型不匹配会编译失败 - 注意:两个子 collector 的
supplier、accumulator等内部逻辑完全隔离,互不影响——这是它比手写reduce安全的地方
容易踩的坑:null、空流、异常传播
teeing 本身不特殊处理边界情况,所有行为由你传入的两个子 collector 决定:
- 如果某路 collector 对空流返回
null(比如自定义 collector 没写finisher且没处理空情形),合并函数里就要判空,否则NullPointerException - 如果其中一路 collector 在 accumulate 阶段抛异常(比如
Collectors.reducing的 identity 为null且元素也为空),整个collect()就炸,不会等第二路 - 别试图在合并函数里做耗时操作或 IO——它在收集结束时才调,但流已经关闭,比如拿
Files.lines()做源,合并函数里再开文件就错
替代方案对比:reduce vs teeing vs 手动遍历
有人觉得用 reduce 自定义容器也能实现类似效果,比如建个 class Result { int sum; double avg; }。问题在于:
-
reduce要求 identity 可变或用new Result(),但并行流下可能多个线程同时修改同一对象,得加锁或改用collect(Supplier, BiConsumer, BiConsumer),代码量翻倍 -
teeing天然支持并行流,两路 collector 各自管理自己的并发策略(比如Collectors.toList()内部用CopyOnWriteArrayList) - 手动 for-loop 看似简单,但丢了流式语义,无法和上游 filter/map 无缝组合,维护成本高
真正难的不是写出来,是意识到什么时候该用它——当你要从同一份数据里提取**语义上无关的两类指标**(比如订单流里同时统计总金额和最晚发货时间),teeing 就是那个不多不少刚刚好的工具。








