Kotlin

[Kotlin] map と flatMap の違いについて【完全理解】

この記事で分かること

  • map と flatMap の動作の違いについて
  • map と flatMap の使い分け方について
  • map と flatMap の違いを理解できるサンプルコードあり

この記事では、Kotlinの map / flatMap の違いについて解説しています。

どちらも値を変換する機能なので、その違いを理解するのが難しいですよね?

このページでは結論を述べた後、サンプルコードを使って両者の違いやユースケースを理解できるようにしています。
KotlinとJavaを比較した例もあるので、参考にしてみて下さい。

ゴイチ

map vs flatMap は定番のテーマ!ぜひ理解して帰ってください!

結論から

Kotlinの flatMap は、map と flatten が一緒になったものと理解できます。

map + flatten = flatMap

続いて、それぞれをどのようなケースで使用するかについて説明します。

ポイント

  • map は、1つの入力が1つの出力になる場合に使用する
  • flatMap は、1つの入力が複数の出力に増える場合に使用する

flatMap を使う場合は例えば、

  • APIに検索条件を渡すと結果が複数返ってくるケース
  • DBを検索すると結果が複数返ってくるケース

などのユースケースが考えられます。
ここまでが結論です。続いてサンプルコードで内容を解説します。

サンプルコード

まずは簡単なサンプルコードを使って、それぞれの動作を理解しましょう。
map と flatMap の動作の違いを説明した後、Java の stream との比較を行います。

基本的な map / flatMap の動作を理解する

fun example() {
    val list = listOf(1, 2, 3)

    // map
    val convertedByMap = list
        .map { listOf(it, it) }
    println(convertedByMap)
    // [[1, 1], [2, 2], [3, 3]]

    // map + flatten
    val convertedByMapFlatten = list
        .map { listOf(it, it) }
        .flatten()
    println(convertedByMapFlatten)
    // [1, 1, 2, 2, 3, 3]

    // flatMap
    val convertedByFlatMap = list
        .flatMap { listOf(it, it) }
    println(convertedByFlatMap)
    // [1, 1, 2, 2, 3, 3]
}

Javaの map / flatMap との比較

Javaで上記のKotlinとほぼ同じコードを書いてみます。
Javaにはflattenが無いため、map と flatMap のコード例のみになります。

public void example() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    // map
    List<List<Integer>> convertedByMap = list
            .stream()
            .map(it -> Arrays.asList(it, it))
            .collect(Collectors.toList());
    System.out.println(convertedByMap);
    // [[1, 1], [2, 2], [3, 3]]

    // flatMap
    List<Integer> convertedByFlatMap = list
            .stream()
            .flatMap(it -> Stream.of(it, it))
            .collect(Collectors.toList());
    System.out.println(convertedByFlatMap);
    // [1, 1, 2, 2, 3, 3]
}

書き方は異なりますが、Kotlinのmap/flatMapと同じ役割を持つと考えていいと思います。

ユースケースの考察

mapは1対1で変換するものなので、比較的用途が想像しやすいと思います。

多くの方は flatMapの用途やユースケースが分かりにくいと思うので、一つサンプルコードを示します。

以下のサンプルコードでは、APIにクエリー文字列を渡すと、レスポンスとしてそのクエリー文字列が含まれるツイートのIDを返すTwitterのAPIがあると仮定してください。

fun example2() {
    val queryList = listOf("Java", "Kotlin", "駆け出しエンジニア")

    val convertedByFlatMap = queryList
        .flatMap { callTwitterApi(it) } // Twitter APIを呼んでるつもり
    println(convertedByFlatMap)
    // [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

private fun callTwitterApi(query: String): List<Long> {
    // Query文字列で検索すると、Queryを含むツイートのIDを返すAPIと仮定する
    return LongRange(1, query.length.toLong()).toList() // APIのレスポンスのつもり
}

1つのクエリー文字列に対して、複数のIDがレスポンスされますが、それらのIDをまとめて一つのListに変換するケースを想定しています。

このケースの様に、flatMapは 1個 -> N個の出力になる場合などに使うと威力を発揮すると思います。

flatMapを使わずにコードを書こうとすると、以下の様になります。

fun example3() {
    val queryList = listOf("Java", "Kotlin", "駆け出しエンジニア")

    val resultList = mutableListOf<Long>()
    for (query in queryList) {
        val ids = callTwitterApi(query)
        resultList.addAll(ids)
    }
    println(resultList)
    // [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

flatMapを使った例の方がかなりシンプルに書けることが分かります。

まとめ

最後に、この記事の内容をまとめます。

要点まとめ

  • flatMap = map + flatten
  • map は 1 -> 1 の変換に使用する
  • flatMapは 1 -> N の変換に使用する

map と flatMap は機能が似ているので、どのように使い分けたらよいか少し難しいです。

今回示したようなユースケースを念頭に置いて map / flatMap を使い分ければ、自信をもってどちらを使うかを選ぶことができると思います。

ゴイチ

実際私も使い分けが不安だったのですが、今回の記事を書くことで理解が深まりました

もし、参考になりましたら幸いです。それでは、他の記事でお会いしましょう!

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++、Java、C#、Kotlinが得意で、組込系からAndroidアプリ、大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-Kotlin
-,