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 の構文

Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

要するに、flatMap は、各要素に対してあなたが Stream を返すことを期待し、それらを 1 本の大きなストリームに「ならして」くれます。

例: 学生のすべての趣味を結合する

List<String> allHobbies = hobbies.stream()
    .flatMap(list -> list.stream())
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [水泳, チェス, サッカー, プログラミング, 読書, 映画]

各趣味リストに対して list.stream() を呼び(その学生の趣味のストリームを得ます)、flatMap がそれらのストリームを 1 本の大きなストリームに「ならして」くれます。これで「趣味のストリーム」ではなく、すべての趣味を直接見られます。

ビジュアルなイメージ

hobbies.stream()
    |
    |---> [水泳, チェス]           -> stream
    |---> [サッカー]               -> stream
    |---> [プログラミング, ...]    -> stream
    |
flatMap: すべてを 1 つの 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 が、得られた文字ストリームを 1 本にまとめます.

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() メソッドがあり、空の要素なら空ストリーム、要素があれば 1 要素のストリームを返します。flatMap は、すべての非空値を 1 本のストリームに統合します。

4. 新メソッド mapMulti: flatMap が不要なとき

なぜ mapMulti が登場したのか?

Java 16 で導入された mapMulti は、flatMap に似ていますが、より効率的で柔軟です。

flatMap は強力ですが、各要素に対して、たとえ 0 件・1 件・複数件を返したいだけでも、新しい Stream を作成しなければならないという欠点があります。中間のコレクションやストリームを作らずに「展開」したい場合、これは非効率になりがちです。

mapMulti は改良版の flatMap で、Consumer を介して結果ストリームに直接値を追加できます。ストリームを返すのではなく、Consumer に「結果として追加すべき要素」を直接渡します。

mapMulti の構文

<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)

各要素ごとに mapper 関数が呼ばれ、その要素自体と、新しい値を「入れていく」ための Consumer が渡されます。

例: 1 回の走査でフィルタと展開を同時に行う

数値のリストがあり、偶数は 2 回、奇数は 0 回(つまりフィルタ)追加したいとします。

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); // 2 回追加する
        }
        // 奇数は何もしない(フィルタ)
    })
    .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 — 各要素を 1 要素に変換するとき。
  • 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
1 要素 単純 いいえ
flatMap
ストリーム (Stream) 結合する はい(中間の Stream)
mapMulti
Consumer (0..n 回) 結合する いいえ(直接追加)

6. flatMap と mapMulti のよくある間違い

間違い 1: Stream<T> の代わりに Stream<Stream<T>> を得てしまう。 コレクションのコレクションを扱うときに、map を使ってしまうケースがよくあります。その結果、余分なループを書かなければならなくなります。

間違い 2: 戻り値の型が不正。 flatMap の関数は Stream を返す必要があり、List や配列を返してはいけません。

間違い 3: 非効率。 単純なケースでは flatMap で十分ですが、各要素ごとに Stream.ofStream.empty() を作らなければならない場合は過剰です。そのような用途には mapMulti の方が適しています。

間違い 4: 古い Java では mapMulti が動かない。 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