Kotlinのmapはとても重要ですが、最初にひとつ整理しておくべき点があります。
Kotlinでは map という言葉が2つの意味で使われます。
- コレクションに対する
map()関数
→ 各要素を別の値に変換するための関数 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]
このとき、
numbersはList<Int>resultはList<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 に対して使ったときは自然に感じますが、Set や Map に使った場合も「元の型を保つ」とは限りません。
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は「何に変えるか」を決める
filter と map を組み合わせる
実際のコードでは、この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]
流れはこうです。
- 偶数だけ残す
- その偶数を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() が向いています。
Sequence と map()
大きなデータを扱うときには 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
)
この prices は read-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 になります。
ここは非常に大事です。
Mapにmap()を使った- だから
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()は変換する
名前は似ていますが別物です。
Map に map() を使うと 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についてでした。
最後までお読みいただき、ありがとうございました。










