1. 前言
假設我們有一份學生清單,每位學生都有自己的興趣清單。例如:
List<List<String>> hobbies = List.of(
List.of("游泳", "西洋棋"),
List.of("足球"),
List.of("程式設計", "閱讀", "電影")
);
你的任務:取得所有興趣的單一清單,以便了解學生普遍在關注什麼。直覺上可以嘗試使用 map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
得到什麼?一個串流的清單!也就是 Stream<Stream<String>>。這不是我們想要的——我們希望拿到單純的 Stream<String>,好直接針對每個興趣操作。
想像你有一個箱子,裡面放著裝有玩具的其他小箱子。map 方法只是把每個小箱子拿出來給你看(Stream<Stream<String>>),而你其實希望直接看到所有玩具(Stream<String>),而不是再去翻箱子。
2. flatMap:如何「展開」巢狀集合
要把「箱子打開」並得到扁平的串流,就需要 flatMap。
它接收一個函式,該函式會對每個元素回傳一個串流,flatMap 會立刻把所有內層串流「攤平」成一個。
flatMap 的語法
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
簡而言之:flatMap 會要求你對每個元素回傳一個 Stream,而它會把這些全部「抹平」成一個大的串流。
範例:合併所有學生的興趣
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [游泳, 西洋棋, 足球, 程式設計, 閱讀, 電影]
對每個興趣清單我們呼叫了 list.stream()(取得某位學生興趣的串流)。而 flatMap 則把所有串流「攤平」成一個大串流——現在我們看到的是所有興趣,而不是一堆興趣串流。
視覺化示意
hobbies.stream()
|
|---> [游泳, 西洋棋] -> stream
|---> [足球] -> stream
|---> [程式設計, ...] -> stream
|
flatMap:將全部合併為單一的 Stream<String>
為什麼 map 解不了這個問題?
如果使用 map,你會得到 Stream<Stream<String>>,而這不好操作:例如你無法立刻遍歷所有字串,還得做額外的走訪。
3. flatMap 的實用範例
把字串拆成字元(List<String> → Stream<Character>)
假設你有一個字串清單,想取得所有字串中出現過的所有字元的串流:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
說明:
- word.chars() 會回傳 IntStream(字元碼的串流)。
- mapToObj 將代碼轉成字元。
- flatMap 把所有取得的字元串流合併為一個。
與 Optional 搭配:Stream<Optional<T>> → Stream<T>
假設你有一個 Optional 物件的清單,並想取得實際存在的那些值的串流:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
重點:
Optional<T>(自 Java 9 起)提供方法 stream(),會回傳空串流或單一元素的串流。flatMap 會把所有非空值合併成一個串流。
4. 全新方法 mapMulti:何時不必用 flatMap
為什麼會有 mapMulti?
在 Java 16 引入的 mapMulti,行為與 flatMap 類似,但稍微更有效率且更彈性。
flatMap 是很強大的工具,但有個缺點:對每個元素你都必須建立一個新的 Stream,即使你只想回傳 0、1 或多個元素。這可能不夠有效率,特別是當你只是想「展開」元素而不建立中介集合或串流時。
mapMulti 是 flatMap 的強化版本,允許你透過 Consumer 直接把需要的值加入結果串流,而不必建立中介結構。你不再回傳串流,而是直接透過 Consumer 指定要加入結果串流的元素。
mapMulti 的語法
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
對每個元素都會呼叫函式 mapper,它會取得元素本身與一個 Consumer,你可以把新值「丟進」這個 Consumer。
範例:一次走訪同時完成過濾與展開
假設你有一串數字,對偶數要加入兩次,對奇數則不加入(也就是同時過濾與展開):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // 加入兩次
}
// 對於奇數不做任何事(過濾)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
與 flatMap 的比較
用 flatMap 寫同樣的需求會是這樣:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
這裡我們仍然必須為每個元素建立 Stream.of(...) 或 Stream.empty(),即使這麼做可能沒有效率。
5. 何時用 flatMap,何時用 mapMulti?
- map:當你把每個元素對應到單一元素時。
- flatMap:當你把每個元素對應到一個元素串流時。
- mapMulti:當你想產生多個元素、但不想建立中介串流時(效率較佳,特別是在熱迴圈中)。
再一個例子:把 Map<Integer, List<String>> 展開為 Stream<Pair(Integer, String)>
假設我們有一個對應表:id → 興趣清單。
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("游泳", "西洋棋"),
2, List.of("足球"),
3, List.of("程式設計", "閱讀", "電影")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [游泳, 西洋棋, 足球, 程式設計, 閱讀, 電影]
如果我們想得到配對 (id, 興趣):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: 游泳, 1: 西洋棋, 2: 足球, 3: 程式設計, 3: 閱讀, 3: 電影]
用 mapMulti(Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
優點:
mapMulti 不會建立中介串流,而是直接把值「丟到」最終串流中。
視覺化:flatMap 與 mapMulti
| 方法 | 函式回傳什麼 | 如何合併 | 有中介集合嗎? |
|---|---|---|---|
|
單一元素 | 直接 | 沒有 |
|
串流 (Stream) | 合併 | 有(中介 Stream) |
|
Consumer (0..n 次) | 合併 | 沒有(直接加入) |
6. 使用 flatMap 與 mapMulti 時的常見錯誤
錯誤 1:得到 Stream<Stream<T>> 而非 Stream<T>。 常見情況是面對集合的集合時,用了 map 而非 flatMap。結果需要寫多餘的迴圈。
錯誤 2:回傳型別不正確。 對 flatMap 而言,函式必須回傳 Stream,而不是 List 或陣列。
錯誤 3:效能不佳。 在簡單情境下 flatMap 很好用,但若你必須為每個元素建立 Stream.of 或 Stream.empty(),可能就太多餘。此時改用 mapMulti 較好。
錯誤 4:mapMulti 在舊版 Java 不可用。 mapMulti 直到 Java 16 才引入。若你的 JDK 版本更舊,就無法使用此方法。
錯誤 5:與 null 有關的問題。 不要從 flatMap 回傳 null——遇到「空」案例時請一律回傳 Stream.empty()。
錯誤 6:不必要的中介集合。 若能透過 mapMulti 中的 consumer 直接加入元素,就不要建立多餘的清單或串流。
GO TO FULL VERSION