CodeGym /課程 /JAVA 25 SELF /flatMap 與 mapMulti 方法

flatMap 與 mapMulti 方法

JAVA 25 SELF
等級 32 , 課堂 0
開放

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 或多個元素。這可能不夠有效率,特別是當你只是想「展開」元素而不建立中介集合或串流時。

mapMultiflatMap 的強化版本,允許你透過 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

方法 函式回傳什麼 如何合併 有中介集合嗎?
map
單一元素 直接 沒有
flatMap
串流 (Stream) 合併 有(中介 Stream)
mapMulti
Consumer (0..n 次) 合併 沒有(直接加入)

6. 使用 flatMap 與 mapMulti 時的常見錯誤

錯誤 1:得到 Stream<Stream<T>> 而非 Stream<T>。 常見情況是面對集合的集合時,用了 map 而非 flatMap。結果需要寫多餘的迴圈。

錯誤 2:回傳型別不正確。flatMap 而言,函式必須回傳 Stream,而不是 List 或陣列。

錯誤 3:效能不佳。 在簡單情境下 flatMap 很好用,但若你必須為每個元素建立 Stream.ofStream.empty(),可能就太多餘。此時改用 mapMulti 較好。

錯誤 4:mapMulti 在舊版 Java 不可用。 mapMulti 直到 Java 16 才引入。若你的 JDK 版本更舊,就無法使用此方法。

錯誤 5:與 null 有關的問題。 不要從 flatMap 回傳 null——遇到「空」案例時請一律回傳 Stream.empty()

錯誤 6:不必要的中介集合。 若能透過 mapMulti 中的 consumer 直接加入元素,就不要建立多餘的清單或串流。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION