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 的函數式編程能力逐漸補齊了短板,在復雜數據處理場景中變得更加順手。