Kotlinのmapについて

採用はこちら

Kotlinのmapはとても重要ですが、最初にひとつ整理しておくべき点があります。

Kotlinでは map という言葉が2つの意味で使われます。

  1. コレクションに対する map() 関数
    → 各要素を別の値に変換するための関数
  2. Map
    → キーと値の対応を持つデータ構造

この2つは名前が似ていますが、意味はまったく別です。

ここを切り分けて理解すると、かなりわかりやすくなります。

目次

map() 関数とは何か

map() は、コレクションの各要素に変換処理を適用して、その結果を新しいリストとして返す関数です。

たとえば、数値のリストを文字列のリストに変換する場合はこう書きます。

val numbers = listOf(1, 2, 3, 4)
val result = numbers.map { "No.$it" }

println(result) // [No.1, No.2, No.3, No.4]

このとき、

  • numbersList<Int>
  • resultList<String>

です。

つまりmap()は、元の要素を1つずつ別の形に変換して、新しい結果を返す関数です。

map() の基本構文

基本形はこうです。

collection.map { element ->
    変換処理
}

もっと簡単に、it を使って書くこともできます。

val numbers = listOf(1, 2, 3)
val doubled = numbers.map { it * 2 }

println(doubled) // [2, 4, 6]

これは各要素に対して

  • 1 → 2
  • 2 → 4
  • 3 → 6

という変換を行っています。

map() のポイント

map()を理解するうえで大事なのは、次の3点です。

各入力要素に対して1回ずつ変換する

基本的に、入力が3個なら出力も3個です。

1つの要素ごとに1つの結果が作られます。

元のコレクションは変更しない

map() は元のデータを書き換えません。

変換結果を持つ新しいリストを返します。

val numbers = listOf(1, 2, 3)
val doubled = numbers.map { it * 2 }

println(numbers) // [1, 2, 3]
println(doubled) // [2, 4, 6]

戻り値は「元と同じコレクション型」とは限らない

ここは少し大事です。

map() は「変換した結果」を返しますが、元が Set でも Map でも、結果は List になることが多いです。

たとえば List に対して使ったときは自然に感じますが、SetMap に使った場合も「元の型を保つ」とは限りません。

forEach との違い

map() とよく比較されるのが forEach です。

forEach

各要素に対して処理をするためのものです。

主に表示やログ出力など、副作用のある処理に使われます。

val numbers = listOf(1, 2, 3)

numbers.forEach {
    println(it)
}

map()

各要素を変換して、新しい結果を作るためのものです。

val numbers = listOf(1, 2, 3)
val result = numbers.map { it * 2 }

println(result) // [2, 4, 6]

この違いはかなり重要です。

  • 何かを実行したいforEach
  • 変換した新しい値がほしいmap

filter との違い

filter は要素を絞り込む関数です。

map は要素を変換する関数です。

val numbers = listOf(1, 2, 3, 4, 5)

val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // [2, 4]

val doubled = numbers.map { it * 2 }
println(doubled) // [2, 4, 6, 8, 10]

違いを一言でいうとこうです。

  • filter は「残すかどうか」を決める
  • map は「何に変えるか」を決める

filtermap を組み合わせる

実際のコードでは、この2つを組み合わせることがよくあります。

val numbers = listOf(1, 2, 3, 4, 5, 6)

val result = numbers
    .filter { it % 2 == 0 }
    .map { it * 10 }

println(result) // [20, 40, 60]

流れはこうです。

  1. 偶数だけ残す
  2. その偶数を10倍に変換する

このように、Kotlinではコレクション処理をつなげて書くことが多いです。

mapIndexed()

要素だけでなく、インデックスも使いたいときは mapIndexed() を使います。

val items = listOf("A", "B", "C")

val result = items.mapIndexed { index, value ->
    "$index: $value"
}

println(result) // [0: A, 1: B, 2: C]

通常のmap()では各要素しか扱えませんが、mapIndexed()なら

  • インデックス

の両方を使えます。

mapNotNull()

変換した結果が null になるものを取り除きたい場合は、mapNotNull() が便利です。

val texts = listOf("1", "2", "abc", "4")

val numbers = texts.mapNotNull { it.toIntOrNull() }

println(numbers) // [1, 2, 4]

これを普通の map() で書くとこうなります。

val numbers = texts.map { it.toIntOrNull() }
println(numbers) // [1, 2, null, 4]

この場合の型は List<Int?> です。

一方、mapNotNull() を使うと null が除外されるので、結果は List<Int> になります。

flatMap() との違い

map() と似ていますが、flatMap() は少し役割が違います。

val words = listOf("abc", "de")

val result1 = words.map { it.toList() }
println(result1) // [[a, b, c], [d, e]]

val result2 = words.flatMap { it.toList() }
println(result2) // [a, b, c, d, e]

違いはこうです。

  • map()
    → 各要素を変換する
    → 結果がネストすることがある
  • flatMap()
    → 各要素を変換したあと、1段平らにする

つまり、1つの要素から複数要素を作り、それをひとつの一覧にまとめたいときは flatMap() が向いています。

Sequencemap()

大きなデータを扱うときには Sequence が使われることがあります。

val result = listOf(1, 2, 3, 4, 5)
    .asSequence()
    .filter { it % 2 == 1 }
    .map { it * 10 }
    .toList()

println(result) // [10, 30, 50]

Sequence の特徴は、処理が遅延評価されることです。

つまり、中間結果のコレクションをその都度作らず、必要になるまで処理を後回しにできます。

ただし、ここは誤解しやすいところです。

  • 常に Sequence の方が速いわけではない
  • 小さなデータでは普通の List 処理の方がシンプルで十分なことが多い
  • 長い変換チェーンや大きなデータでは有利になることがある

なので、Sequence は「いつでも使うべきもの」ではなく、状況に応じて使い分けるものと考えるのが自然です。

Map 型とは何か

ここからは map() 関数ではなく、Mapの話です。

Map は、キーと値の対応を保持するコレクションです。

val userAges = mapOf(
    "田中" to 25,
    "佐藤" to 30,
    "鈴木" to 28
)

println(userAges["田中"]) // 25

この場合は

  • キー: "田中"
  • 値: 25

です。

たとえば「名前から年齢を取り出したい」というように、ある値をキーにして別の値を引きたいときに使います。

mapOf()mutableMapOf()

ここも大事です。

mapOf()

読み取り専用の Map を作ります。

val prices = mapOf(
    "apple" to 100,
    "banana" to 150
)

この pricesread-only な Map です。

この参照を通して追加や更新はできません。

ただし、ここでいう read-only は「変更操作を提供しない」という意味です。

「絶対に不変である」と強く言い切るのとは少し違います。

初心者向けには「変更できないMap」と考えて大きな問題はありませんが、厳密には read-only interface と理解する方が正確です。

mutableMapOf()

変更可能な MutableMap を作ります。

val prices = mutableMapOf(
    "apple" to 100,
    "banana" to 150
)

prices["orange"] = 200
prices["apple"] = 120

println(prices) // {apple=120, banana=150, orange=200}

Map の基本操作

値を取得する

val prices = mapOf(
    "apple" to 100,
    "banana" to 150
)

println(prices["apple"])  // 100
println(prices["orange"]) // null

存在しないキーを指定すると null になります。

デフォルト値を使う

println(prices.getOrDefault("orange", 0)) // 0

追加・更新する

val prices = mutableMapOf(
    "apple" to 100
)

prices["banana"] = 150
prices["apple"] = 120

削除する

prices.remove("apple")

Map をループする

Map はキーと値の組として回せます。

val userAges = mapOf(
    "田中" to 25,
    "佐藤" to 30
)

for ((name, age) in userAges) {
    println("$name は $age 歳です")
}

あるいは forEach でも書けます。

userAges.forEach { (name, age) ->
    println("$name は $age 歳です")
}

Map に対して map() を使うとどうなるか

ここはかなり混乱しやすいポイントです。

Map に対しても map() を使えますが、返ってくるのは Map ではなく List です。

val userAges = mapOf(
    "田中" to 25,
    "佐藤" to 30
)

val result = userAges.map { (name, age) ->
    "$name:$age"
}

println(result) // [田中:25, 佐藤:30]

これは Map の各要素、つまり各 Entry を変換しているからです。

結果は「変換結果の一覧」なので、List になります。

ここは非常に大事です。

  • Mapmap() を使った
  • だから Map が返る

とは限りません。

実際には List が返る と理解してください。

mapValues()mapKeys()

Map の構造を保ったまま変換したいときは、map() ではなくこちらを使います。

mapValues()

値だけを変換する

val prices = mapOf(
    "apple" to 100,
    "banana" to 150
)

val doubled = prices.mapValues { (_, value) ->
    value * 2
}

println(doubled) // {apple=200, banana=300}

mapKeys()

キーだけを変換する

val prices = mapOf(
    "apple" to 100,
    "banana" to 150
)

val upperKeys = prices.mapKeys { (key, _) ->
    key.uppercase()
}

println(upperKeys) // {APPLE=100, BANANA=150}

mapKeys() の注意点

これは補足としてかなり重要です。

mapKeys() で変換後のキーが重複した場合、後の値で上書きされます。

val m = mapOf(
    "a" to 1,
    "A" to 2
)

val result = m.mapKeys { (key, _) ->
    key.uppercase()
}

println(result) // {A=2}

元は "a""A" で別キーですが、どちらも大文字にすると "A" になります。

その結果、後の値が残ります。

実務では意外と見落としやすいポイントです。

associateBy()associateWith()

これらは map() とは少し目的が違います。

リストなどから Map を作るための関数です。

associateBy()

要素からキーを作って Map にする

data class User(val id: Int, val name: String)

val users = listOf(
    User(1, "田中"),
    User(2, "佐藤")
)

val userMap = users.associateBy { it.id }

println(userMap)
// {1=User(id=1, name=田中), 2=User(id=2, name=佐藤)}

associateWith()

各要素をキーにして値を作る

val words = listOf("apple", "banana")

val lengths = words.associateWith { it.length }

println(lengths) // {apple=5, banana=6}

つまり、

  • map() は変換して一覧を作る
  • associateBy()associateWith()Map を作る

という違いがあります。

実務でよくある使い方

APIレスポンスを画面表示用のデータに変換する

data class ApiUser(val id: Int, val firstName: String, val lastName: String)
data class UserViewModel(val id: Int, val fullName: String)

val apiUsers = listOf(
    ApiUser(1, "Taro", "Tanaka"),
    ApiUser(2, "Hanako", "Sato")
)

val viewModels = apiUsers.map {
    UserViewModel(
        id = it.id,
        fullName = "${it.firstName} ${it.lastName}"
    )
}

これはかなりよくある書き方です。

取得したデータを、そのままUIに使いやすい形へ変換しています。

文字列を安全に数値へ変換する

val input = listOf("10", "20", "abc", "40")

val values = input.mapNotNull { it.toIntOrNull() }

println(values) // [10, 20, 40]

不正な値を自然に除外できるので便利です。

IDで素早く引ける形に変換する

data class Product(val id: Int, val name: String)

val products = listOf(
    Product(1, "Pen"),
    Product(2, "Notebook")
)

val productMap = products.associateBy { it.id }

println(productMap[2]) // Product(id=2, name=Notebook)

検索しやすい形にしたいときに使います。

よくある間違い

map() を呼んだだけで元が変わると思ってしまう

val numbers = listOf(1, 2, 3)
numbers.map { it * 2 }

println(numbers) // [1, 2, 3]

これは変わりません。

map() の戻り値を受け取る必要があります。

val doubled = numbers.map { it * 2 }

map() の中で副作用だけを行う

numbers.map {
    println(it)
}

動きますが、これは forEach の方が自然です。

Map 型と map() 関数を混同する

  • mapOf()Map を作る
  • map() は変換する

名前は似ていますが別物です。

Mapmap() を使うと Map が返ると思ってしまう

実際には List が返ります。

Map を保ちたいなら mapValues()mapKeys() を使います。

まとめ

Kotlinのmapを理解するうえで、一番大事なのは次の整理です。

map() 関数

各要素を変換して、新しい結果を作る関数です。

val result = listOf(1, 2, 3).map { it * 2 }

Map

キーと値の対応を持つコレクションです。

val ages = mapOf("田中" to 25, "佐藤" to 30)

Map に対しての変換

  • map() → 各 entry を変換して List を返す
  • mapValues() → 値を変換して Map を保つ
  • mapKeys() → キーを変換して Map を保つ

リストから Map を作る

  • associateBy()
  • associateWith()

最後に一言でいうと

map()「コレクションの各要素を別の形に変換する関数」です。

一方で Map「キーと値の対応を持つデータ構造」です。

この2つを分けて考えると、Kotlinのmapはかなり理解しやすくなります。

以上、Kotlinのmapについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次