Stream API 新玩法:从 teeing()到 mapMulti()
1. 背景:Stream API 的演进
自 Java 8 引入 Stream API
以来,Java 的集合处理方式发生了质变。开发者可以用声明式风格实现复杂的数据转换与聚合。然而,随着应用场景多样化,社区逐渐发现一些“尴尬空缺”:
- 聚合时,往往需要计算两个指标(比如总数与平均值),却只能先
.collect()
一次,再写逻辑合并,代码冗余。 - 一对多映射场景(比如把一条日志展开成多条事件),过去只能用
flatMap()
,但其表达力稍显笨重。
从 Java 12 开始,JDK 对 Stream
的功能进行了增强,加入了更灵活的收集器和映射器。其中最值得关注的就是 teeing()
和 mapMulti()
。
2. teeing()
收集器 —— 一次遍历,双份结果
2.1 用途
teeing()
是 Java 12 引入的新收集器。它允许我们在一次流处理过程中,同时执行两个独立的收集操作,并在结束时用一个合并函数把结果“合并”起来。
2.2 传统写法(累赘)
假设我们要计算一组订单的 总金额 和 平均金额:
List<Integer> orders = List.of(100, 200, 300, 400);// 传统写法:需要遍历两次
int sum = orders.stream().mapToInt(i -> i).sum();
double avg = orders.stream().mapToInt(i -> i).average().orElse(0);
这里 orders.stream()
被遍历了两次,在大数据场景下显得低效。
2.3 teeing()
写法(优雅)
import static java.util.stream.Collectors.*;var result = orders.stream().collect(teeing(summingInt(Integer::intValue), // 收集器1:求和averagingInt(Integer::intValue), // 收集器2:平均(sum, avg) -> String.format("Sum=%d, Avg=%.2f", sum, avg)));System.out.println(result);
// 输出: Sum=1000, Avg=250.00
一次遍历,得到两个收集结果,再由合并函数包装成最终对象。
2.4 适用场景
- 统计类场景(如最大值 & 最小值、总数 & 平均值)
- 报表生成(一次聚合,多维指标输出)
- 性能敏感场景(避免多次遍历)
3. mapMulti()
—— 更强大的 “一对多” 映射
3.1 背景
在 Java 8 的 Stream
中,如果要把一条记录映射成多条,需要 flatMap()
:
Stream.of("a:b:c", "d:e").flatMap(s -> Arrays.stream(s.split(":"))).forEach(System.out::println);
flatMap()
可以解决问题,但表达能力有限:
- 必须返回一个流
Stream<T>
,有时仅仅想用条件判断推送几个元素,却要额外包装。 - 需要注意额外的对象创建(比如数组或中间 Stream)。
3.2 mapMulti()
简化表达
mapMulti()
从 Java 16 引入,它的思路是:你负责“往下游收集器里塞元素”,JDK 不强制你非得返回一个 Stream。
Stream.of("a:b:c", "d:e").mapMulti((str, downstream) -> {for (String part : str.split(":")) {downstream.accept(part);}}).forEach(System.out::println);
效果等价于上面 flatMap()
示例,但避免了额外的 Arrays.stream()
。
3.3 高级用法 —— 条件展开
比如:只展开长度大于 1 的子串。
Stream.of("x:yz:abc", "m:n").mapMulti((str, downstream) -> {for (String part : str.split(":")) {if (part.length() > 1) {downstream.accept(part);}}}).forEach(System.out::println);
// 输出: yz, abc
这种写法,逻辑更直观,避免创建一堆中间集合/流。
3.4 适用场景
- 日志/文本解析:一行可能映射成多条事件。
- 过滤展开:部分元素不展开,部分元素展开多份。
- 高性能场景:避免额外
Stream
对象的开销。
4. 对比小结
特性 | teeing() | mapMulti() |
---|---|---|
引入版本 | Java 12 | Java 16 |
作用 | 一次遍历,双收集结果 | 高效一对多映射 |
替代 | 两次 .collect() 或手写聚合逻辑 | flatMap() (但更直观、更高效) |
典型应用 | 统计报表、聚合指标 | 日志解析、条件展开 |
5. flatMap()
vs mapMulti()
性能对比
我们写一段小测试,生成一百万条带分隔符的字符串,用 flatMap()
和 mapMulti()
分别展开,观察耗时。
import java.util.*;
import java.util.stream.*;
import java.time.*;public class MapMultiVsFlatMap {public static void main(String[] args) {// 构造一百万条字符串List<String> data = IntStream.range(0, 1_000_000).mapToObj(i -> "a:b:c:d:e").toList();// 测试 flatMaplong t1 = System.currentTimeMillis();long count1 = data.stream().flatMap(s -> Arrays.stream(s.split(":"))).count();long t2 = System.currentTimeMillis();// 测试 mapMultilong t3 = System.currentTimeMillis();long count2 = data.stream().mapMulti((s, downstream) -> {for (String part : s.split(":")) {downstream.accept(part);}}).count();long t4 = System.currentTimeMillis();System.out.printf("flatMap count=%d, time=%dms%n", count1, (t2 - t1));System.out.printf("mapMulti count=%d, time=%dms%n", count2, (t4 - t3));}
}
预期结果(不同机器可能略有差异):
flatMap count=5000000, time=242ms
mapMulti count=5000000, time=181ms
可以看到,在数据量大时,mapMulti()
由于避免了中间 Stream
对象的构建,通常会比 flatMap()
更快一些,且内存占用也更低。
6. 总结
Stream API 在 Java 12 之后不断进化:
teeing()
让我们可以一次性得到多个收集结果,既优雅又高效;mapMulti()
则让“一对多”的映射逻辑更加自然简洁。
随着这些增强,Java 的函数式编程能力逐渐补齐了短板,在复杂数据处理场景中变得更加顺手。