- Kotlin
Kotlin List・Mapの使い方とコレクション操作一覧【早見表】
KotlinのListは順番に並んだ要素の集まり、Mapはキーと値の組の集まりで、どちらも「読み取り専用(listOf・mapOf)」と「変更できる(mutableListOf・mutableMapOf)」の2種類に分かれています。 追加や削除をしたいなら mutable のほう、中身を変えないなら読み取り専用のほうを選ぶのが基本です。
この記事では、List・Map を作る関数の違いを「変更可否・実体の型」で並べた早見表と、map・filter・groupBy など約70個の操作を「やりたいこと→書き方→出力」で引ける逆引き一覧をまとめました。一覧の出力はすべて実際に実行したものです。あわせて、読み取り専用 List に add したときのコンパイルエラー、first() の NoSuchElementException と firstOrNull() の対比、Sequence との評価順の違いも出力で示します。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc-jvm)/ OpenJDK 21.0.10。掲載したコードはすべてこの環境でコンパイル・実行し、出力をそのまま載せています。実行コマンドは kotlinc sample.kt -include-runtime -d sample.jar && java -jar sample.jar の形です。
List・Map を作る関数の早見表
「実体の型」は、上の確認環境(JVM)で javaClass.name を表示した結果です。
| 作り方 | 変数の型 | 追加・削除 | 実体の型(JVM) | 向いている場面 |
|---|---|---|---|---|
listOf(1, 2, 3) |
List<Int> |
できない | java.util.Arrays$ArrayList | 中身を変えないリスト |
listOf(1) |
List<Int> |
できない | java.util.Collections$SingletonList | 1件だけのリスト |
emptyList<Int>() / listOf() |
List<Int> |
できない | kotlin.collections.EmptyList | 空のリストを返す |
mutableListOf(1, 2, 3) |
MutableList<Int> |
できる | java.util.ArrayList | あとから追加・削除する |
arrayListOf(1, 2, 3) |
ArrayList<Int> |
できる | java.util.ArrayList | ArrayList 型が必要なとき |
buildList { add(1) } |
List<Int> |
作るときだけできる | kotlin.collections.builders.ListBuilder | 条件つきで組み立てて、読み取り専用で返す |
mapOf("a" to 1, "b" to 2) |
Map<String, Int> |
できない | java.util.LinkedHashMap | 中身を変えない Map(入れた順を保つ) |
emptyMap<String, Int>() |
Map<String, Int> |
できない | kotlin.collections.EmptyMap | 空の Map を返す |
mutableMapOf("a" to 1) |
MutableMap<String, Int> |
できる | java.util.LinkedHashMap | あとから追加・削除する(入れた順を保つ) |
sortedMapOf("b" to 2, "a" to 1) |
SortedMap<String, Int> |
できる | java.util.TreeMap | キー順に並べたい |
buildMap { put("a", 1) } |
Map<String, Int> |
作るときだけできる | kotlin.collections.builders.MapBuilder | 組み立てて読み取り専用で返す |
迷ったら listOf / mapOf から始め、追加や削除が必要になったら mutableListOf / mutableMapOf に変える、という順番で十分です。mutableListOf と arrayListOf は実体が同じ java.util.ArrayList で、違いは変数の型(MutableList か ArrayList か)だけです。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin List・Mapの使い方とコレクション操作一覧【早見表】」
まず動かす:よく使う操作を1本のコードで
次のコードは、そのまま貼り付けて動かせます。絞り込み、並べ替え、グループ化、Map 化、集計の代表的な形が入っています。
data class Item(val name: String, val category: String, val price: Int)
fun main() {
val items = listOf(
Item("pen", "文具", 120), Item("note", "文具", 300),
Item("cup", "食器", 500), Item("dish", "食器", 800),
)
// 絞り込み → 並べ替え → 変換
val names = items.filter { it.price >= 300 }.sortedByDescending { it.price }.map { it.name }
println(names)
// カテゴリごとの合計
val totals = items.groupBy { it.category }.mapValues { (_, list) -> list.sumOf { it.price } }
println(totals)
// 名前で引ける Map
val byName = items.associateBy { it.name }
println(byName["cup"]?.price)
// 2つに分ける
val (cheap, others) = items.partition { it.price < 400 }
println("${cheap.map { it.name }} / ${others.map { it.name }}")
// 合計・最大・連結
println(items.sumOf { it.price })
println(items.maxByOrNull { it.price }?.name)
println(items.joinToString(" / ") { "${it.name}:${it.price}" })
}
実行結果:
[dish, cup, note]
{文具=420, 食器=1300}
500
[pen, note] / [cup, dish]
1720
dish
pen:120 / note:300 / cup:500 / dish:800
Kotlin のコレクション操作は、もとのリストを変えずに新しいリストや Map を返します。 そのため filter → sortedByDescending → map のように、ドットでつないで書けます。
listOf・mutableListOf・arrayListOf・buildList・emptyList の違い
早見表のもとになったコードと出力です。List・Map・Set の作り方を一通り並べています。
fun t(label: String, v: Any) = println("${label.padEnd(14)}: ${v.javaClass.name} $v")
fun main() {
t("listOf(3件)", listOf(1, 2, 3))
t("listOf(1件)", listOf(1))
t("listOf()", listOf<Int>())
t("emptyList", emptyList<Int>())
t("mutableListOf", mutableListOf(1, 2, 3))
t("arrayListOf", arrayListOf(1, 2, 3))
t("buildList", buildList { add(1); add(2) })
t("List(3){it*10}", List(3) { it * 10 })
t("listOfNotNull", listOfNotNull(1, null, 3))
t("mapOf(2件)", mapOf("a" to 1, "b" to 2))
t("emptyMap", emptyMap<String, Int>())
t("mutableMapOf", mutableMapOf("a" to 1))
t("hashMapOf", hashMapOf("a" to 1))
t("sortedMapOf", sortedMapOf("b" to 2, "a" to 1))
t("buildMap", buildMap { put("a", 1) })
t("setOf", setOf(3, 1, 3))
t("mutableSetOf", mutableSetOf(3, 1, 3))
t("hashSetOf", hashSetOf(3, 1, 3))
}
実行結果:
listOf(3件) : java.util.Arrays$ArrayList [1, 2, 3]
listOf(1件) : java.util.Collections$SingletonList [1]
listOf() : kotlin.collections.EmptyList []
emptyList : kotlin.collections.EmptyList []
mutableListOf : java.util.ArrayList [1, 2, 3]
arrayListOf : java.util.ArrayList [1, 2, 3]
buildList : kotlin.collections.builders.ListBuilder [1, 2]
List(3){it*10}: java.util.ArrayList [0, 10, 20]
listOfNotNull : java.util.ArrayList [1, 3]
mapOf(2件) : java.util.LinkedHashMap {a=1, b=2}
emptyMap : kotlin.collections.EmptyMap {}
mutableMapOf : java.util.LinkedHashMap {a=1}
hashMapOf : java.util.HashMap {a=1}
sortedMapOf : java.util.TreeMap {a=1, b=2}
buildMap : kotlin.collections.builders.MapBuilder {a=1}
setOf : java.util.LinkedHashSet [3, 1]
mutableSetOf : java.util.LinkedHashSet [3, 1]
hashSetOf : java.util.HashSet [1, 3]
出力から分かることを整理します。
listOfは件数で実体が変わります。 0件は EmptyList、1件は SingletonList、3件のときは Arrays$ArrayList でした。どれも変数の型はListで、使い方は同じです。mutableListOfとarrayListOfはどちらも java.util.ArrayList です。公式ドキュメントでも、MutableList の既定の実装は ArrayList と説明されています。mapOfとmutableMapOfはどちらも LinkedHashMap で、入れた順番を保ちます。setOfとmutableSetOfも LinkedHashSet で、setOf(3, 1, 3)は[3, 1]と入れた順でした。順序を持たないhashSetOf(3, 1, 3)は[1, 3]です。sortedMapOfは TreeMap で、"b"を先に入れてもキー順の{a=1, b=2}になりました。
実体の型は実装の詳細なので、コードでは変数の型(List か MutableList か)で扱いを決めるのがおすすめです。
読み取り専用 List に add するとコンパイルエラーになる
listOf で作ったリストには、add や [0] = ... がありません。書くとコンパイルの段階で止まります。
fun main() {
val list = listOf(1, 2, 3)
list.add(4)
list[0] = 9
val map = mapOf("a" to 1)
map["b"] = 2
}
ro.kt:3:10: error: unresolved reference 'add' on receiver of type 'List<Int>'.
list.add(4)
^^^
ro.kt:4:9: error: no 'set' operator method providing array access.
list[0] = 9
^^^
ro.kt:6:8: error: no 'set' operator method providing array access.
map["b"] = 2
^^^^^
| エラー文 | 意味 | 直し方 |
|---|---|---|
unresolved reference 'add' on receiver of type 'List<Int>' |
List 型に add という関数はない |
mutableListOf にする、+ で新しいリストを作る |
no 'set' operator method providing array access |
list[0] = 9 や map["b"] = 2 に使う set がない |
mutableListOf / mutableMapOf にする |
直し方3通りと、val・var の違い
fun main() {
// 直し方1: 最初から mutableListOf にする
val m = mutableListOf(1, 2, 3)
m.add(4)
m[0] = 9
println(m)
// 直し方2: 読み取り専用のまま、+ で新しいリストを作る
val a = listOf(1, 2, 3)
val b = a + 4
println("$a -> $b")
// 直し方3: toMutableList でコピーしてから変更
val c = a.toMutableList()
c.add(4)
println("$a / $c")
// Map も同じ
val map = mapOf("a" to 1)
val map2 = map + ("b" to 2)
val mm = map.toMutableMap().apply { this["c"] = 3 }
println("$map / $map2 / $mm")
// val と var、List と MutableList は別の話
var v = listOf(1)
v += 2 // 新しいリストを作って変数に入れ直す
println(v)
val w = mutableListOf(1)
w += 2 // 同じリストに追加する
println(w)
}
実行結果:
[9, 2, 3, 4]
[1, 2, 3] -> [1, 2, 3, 4]
[1, 2, 3] / [1, 2, 3, 4]
{a=1} / {a=1, b=2} / {a=1, c=3}
[1, 2]
[1, 2]
val / var は「変数を別のものに入れ替えられるか」、List / MutableList は「中身を変えられるか」で、別々の話です。 val で宣言した mutableListOf に add できるのはそのためです。
読み取り専用は「変更の入口がない」という意味
List 型は「この変数からは変更できない」ことを表します。もとが MutableList なら、もとの側の変更は見えます。
fun main() {
val m = mutableListOf(1, 2, 3)
val view: List<Int> = m // 型だけ読み取り専用。中身は同じリスト
val snap: List<Int> = m.toList() // その時点のコピー
m.add(4)
println(view) // [1, 2, 3, 4] 元の変更が見える
println(snap) // [1, 2, 3]
// listOf の実体は Java の配列ベースのリスト。キャストして add すると実行時に止まる
val ro = listOf(1, 2, 3)
try {
(ro as MutableList<Int>).add(4)
} catch (e: UnsupportedOperationException) {
println("UnsupportedOperationException")
}
// buildList の結果も変更を受け付けない
val built = buildList { add(1) }
try {
(built as MutableList<Int>).add(2)
} catch (e: UnsupportedOperationException) {
println("UnsupportedOperationException (buildList)")
}
}
実行結果:
[1, 2, 3, 4]
[1, 2, 3]
UnsupportedOperationException
UnsupportedOperationException (buildList)
val view: List<Int> = mは同じリストを別の型で見ているだけなので、m.add(4)のあとviewも[1, 2, 3, 4]になります。その時点の内容を固定したいときはtoList()でコピーします。listOfやbuildListの結果を MutableList にキャストして add すると、実行時に UnsupportedOperationException で止まりました。キャストせず、toMutableList()でコピーを作るのが安全です。
MutableList の追加・更新・削除
fun main() {
val list = mutableListOf("b", "a")
list.add("c") ; println(list)
list.add(0, "z") ; println(list)
list.addAll(listOf("d", "a")); println(list)
list += "e" ; println(list)
list[1] = "B" ; println(list)
list.remove("a") ; println(list) // 最初の1件だけ
list.removeAt(0) ; println(list)
list.removeAll { it == "a" }; println(list)
list.removeLast() ; println(list)
list.sort() ; println(list) // その場で並べ替え
list.reverse() ; println(list)
list.retainAll { it != "d" }; println(list)
list.clear() ; println(list)
// Int のリストでは remove(値)と removeAt(位置)を区別
val nums = mutableListOf(10, 20, 30)
nums.remove(20)
nums.removeAt(0)
println(nums)
// ループ中の削除
val xs = mutableListOf(1, 2, 3, 4)
try {
for (x in xs) if (x % 2 == 0) xs.remove(x)
} catch (e: ConcurrentModificationException) {
println("ConcurrentModificationException")
}
val ys = mutableListOf(1, 2, 3, 4)
ys.removeAll { it % 2 == 0 }
println(ys)
}
実行結果:
[b, a, c]
[z, b, a, c]
[z, b, a, c, d, a]
[z, b, a, c, d, a, e]
[z, B, a, c, d, a, e]
[z, B, c, d, a, e]
[B, c, d, a, e]
[B, c, d, e]
[B, c, d]
[B, c, d]
[d, c, B]
[c, B]
[]
[30]
ConcurrentModificationException
[1, 3]
各行のコメントと出力を見比べると、remove("a") は最初の1件だけ、removeAll { } は条件に合うものすべて、sort() と reverse() はその場で並べ替え、という違いが分かります。新しいリストがほしいときは sorted() / reversed() を使います。
気を付けたい点が2つあります。
- Int のリストでは
remove(20)は「値が 20 の要素」、removeAt(0)は「0番目の要素」です。上のコードでは[10, 20, 30]から 20 と先頭を消して[30]になりました。 - for でループしながら同じリストから remove すると、ConcurrentModificationException で止まります。 条件で消すときは
removeAll { ... }を使うと、[1, 3]のように意図どおりに消せます。
Map の使い方:取り出し・絞り込み・変換の逆引き
入力は val stock = mapOf("pen" to 12, "note" to 0, "cup" to 5) です。出力はすべて実行結果です。
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| キーで値を取る(なければ null) | stock["pen"] to stock["zzz"] |
(12, null) |
| なければ既定値 | stock.getOrDefault("zzz", 0) |
0 |
| なければ計算した値 | stock.getOrElse("zzz") { -1 } |
-1 |
| キーがあるか | "pen" in stock |
true |
| 値があるか | stock.containsValue(5) |
true |
| キーの一覧 | stock.keys |
[pen, note, cup] |
| 値の一覧 | stock.values |
[12, 0, 5] |
| キーと値で絞る | stock.filter { (_, v) -> v > 0 } |
{pen=12, cup=5} |
| キーで絞る | stock.filterKeys { it.length == 3 } |
{pen=12, cup=5} |
| 値で絞る | stock.filterValues { it == 0 } |
{note=0} |
| 値だけ変換 | stock.mapValues { it.value * 10 } |
{pen=120, note=0, cup=50} |
| キーだけ変換 | stock.mapKeys { it.key.uppercase() } |
{PEN=12, NOTE=0, CUP=5} |
| List に変換 | stock.map { (k, v) -> "$k:$v" } |
[pen:12, note:0, cup:5] |
| 組のリストに変換 | stock.toList() |
[(pen, 12), (note, 0), (cup, 5)] |
| 値の大きい順にキーを並べる | stock.entries.sortedByDescending { it.value }.map { it.key } |
[pen, cup, note] |
| キー順に並べた Map | stock.toSortedMap() |
{cup=5, note=0, pen=12} |
| 1件足した新しい Map | stock + ("dish" to 3) |
{pen=12, note=0, cup=5, dish=3} |
| 1件除いた新しい Map | stock - "note" |
{pen=12, cup=5} |
| 値が最大のキー | stock.maxByOrNull { it.value }?.key |
pen |
| 条件に合う件数 | stock.count { it.value > 0 } |
2 |
stock["zzz"] はキーがないとき null を返します。 そのため戻り値の型は Int? で、既定値がほしいときは getOrDefault か ?: 0 を使います。mapValues と mapKeys は Map を返し、map は List を返す点も区別しておくと便利です。
MutableMap の追加・更新・削除
fun main() {
val m = mutableMapOf("pen" to 1)
m["note"] = 2 ; println(m) // 追加
m["pen"] = m.getValue("pen") + 1 ; println(m) // 更新
m.getOrPut("cup") { 0 } ; println(m) // なければ入れる
m.remove("note") ; println(m) // 削除
m.putAll(mapOf("dish" to 9)) ; println(m) // まとめて追加
m.merge("pen", 10, Int::plus) ; println(m) // あれば合算、なければ追加
for ((k, v) in m) print("$k=$v ")
println()
// 出現回数を数える定番の2通り
val counts = mutableMapOf<Char, Int>()
for (ch in "banana") counts[ch] = (counts[ch] ?: 0) + 1
println(counts)
println("banana".groupingBy { it }.eachCount())
// getValue はキーがないと例外。withDefault で既定値を持たせられる
val stock = mapOf("pen" to 12)
try { stock.getValue("zzz") } catch (e: NoSuchElementException) { println("NoSuchElementException: ${e.message}") }
val d = stock.withDefault { 0 }
println(d.getValue("zzz"))
println(d["zzz"])
}
実行結果:
{pen=1, note=2}
{pen=2, note=2}
{pen=2, note=2, cup=0}
{pen=2, cup=0}
{pen=2, cup=0, dish=9}
{pen=12, cup=0, dish=9}
pen=12 cup=0 dish=9
{b=1, a=3, n=2}
{b=1, a=3, n=2}
NoSuchElementException: Key zzz is missing in the map.
0
null
getValue はキーがないと「Key zzz is missing in the map.」というメッセージの NoSuchElementException になります。withDefault { 0 } を付けた Map では getValue が既定値を返しますが、[] での取り出しは null のままでした(出力の最後の2行)。
コレクション操作の逆引き一覧
ここからが操作関数の一覧です。入力は次の3つで、表の「出力」は各式を実際に実行して表示した結果です。
data class Item(val name: String, val category: String, val price: Int)
val nums = listOf(5, 3, 8, 3, 1)
val words = listOf("apple", "banana", "cherry", "avocado")
val items = listOf(
Item("pen", "文具", 120), Item("note", "文具", 300),
Item("cup", "食器", 500), Item("dish", "食器", 800),
)
出力の (5, 1) のようなかっこは Pair(2つ組)です。A to B と書くと Pair になります。
変換する(map 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 各要素を変換 | nums.map { it * 2 } |
[10, 6, 16, 6, 2] |
| 位置も使って変換 | nums.mapIndexed { i, v -> i * v } |
[0, 3, 16, 9, 4] |
| 変換して null を除く | listOf("1", "x", "3").mapNotNull { it.toIntOrNull() } |
[1, 3] |
| 入れ子を1段平らに | listOf(listOf(1, 2), listOf(3)).flatten() |
[1, 2, 3] |
| 変換してから平らに | listOf("ab", "cd").flatMap { it.toList() } |
[a, b, c, d] |
| 2つのリストを組にする | nums.zip(words) |
[(5, apple), (3, banana), (8, cherry), (3, avocado)] |
| 隣どうしを組にする | nums.zipWithNext() |
[(5, 3), (3, 8), (8, 3), (3, 1)] |
| 組のリストを2つに分ける | listOf(1 to "a", 2 to "b").unzip() |
([1, 2], [a, b]) |
| 文字列に連結 | words.joinToString(", ", "[", "]") { it.uppercase() } |
[APPLE, BANANA, CHERRY, AVOCADO] |
絞り込む(filter 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 条件に合う要素 | nums.filter { it > 3 } |
[5, 8] |
| 条件に合わない要素 | nums.filterNot { it > 3 } |
[3, 3, 1] |
| 位置で絞る | nums.filterIndexed { i, _ -> i % 2 == 0 } |
[5, 8, 1] |
| null を除く | listOf(1, null, 3).filterNotNull() |
[1, 3] |
| 型で絞る | listOf(1, "a", 2).filterIsInstance<Int>() |
[1, 2] |
| 合う・合わないの2つに分ける | nums.partition { it % 2 == 0 } |
([8], [5, 3, 3, 1]) |
| 重複を除く | nums.distinct() |
[5, 3, 8, 1] |
| キーで重複を除く | words.distinctBy { it.first() } |
[apple, banana, cherry] |
取り出す(first・take 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 先頭と末尾 | nums.first() to nums.last() |
(5, 1) |
| 条件に合う最初の1件(なければ null) | nums.firstOrNull { it > 100 } |
null |
| 同上(find は firstOrNull と同じ) | nums.find { it > 4 } |
5 |
| 範囲外なら null | nums.getOrNull(10) |
null |
| 範囲外なら既定値 | nums.getOrElse(10) { -1 } |
-1 |
| 値の位置 | nums.indexOf(3) to nums.lastIndexOf(3) |
(1, 3) |
| 条件に合う最初の位置 | nums.indexOfFirst { it > 5 } |
2 |
| ちょうど1件を取り出す | nums.single { it == 8 } |
8 |
| 先頭から n 件 | nums.take(2) |
[5, 3] |
| 先頭 n 件を除く | nums.drop(2) |
[8, 3, 1] |
| 末尾から n 件 | nums.takeLast(2) |
[3, 1] |
| 条件が続く間だけ取る | nums.takeWhile { it > 2 } |
[5, 3, 8, 3] |
| 条件が続く間は捨てる | nums.dropWhile { it > 2 } |
[1] |
| 位置の範囲で取る | nums.slice(1..3) |
[3, 8, 3] |
調べる(any・all 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 1つでも合うか | nums.any { it > 7 } |
true |
| すべて合うか | nums.all { it > 0 } |
true |
| 1つも合わないか | nums.none { it > 10 } |
true |
| 含まれるか | 3 in nums |
true |
| 空かどうか | nums.isEmpty() to nums.isNotEmpty() |
(false, true) |
| 空リストの all | emptyList<Int>().all { it > 0 } |
true |
集計する(sum・fold 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 件数 | nums.count() |
5 |
| 条件に合う件数 | nums.count { it == 3 } |
2 |
| 合計と平均 | nums.sum() to nums.average() |
(20, 4.0) |
| 項目を選んで合計 | items.sumOf { it.price } |
1720 |
| 最大と最小 | nums.max() to nums.min() |
(8, 1) |
| 空でも安全な最大 | emptyList<Int>().maxOrNull() |
null |
| 項目が最大の要素 | items.maxBy { it.price }.name |
dish |
| 項目が最小の要素(空なら null) | items.minByOrNull { it.price }?.name |
pen |
| 項目の最大値そのもの | items.maxOf { it.price } |
800 |
| 初期値つきでたたみ込む | nums.fold(100) { acc, v -> acc + v } |
120 |
| 先頭を初期値にたたみ込む | nums.reduce { acc, v -> acc + v } |
20 |
| 途中経過も残す(累積和) | nums.runningFold(0) { acc, v -> acc + v } |
[0, 5, 8, 16, 19, 20] |
並べ替える(sorted 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| 昇順 | nums.sorted() |
[1, 3, 3, 5, 8] |
| 降順 | nums.sortedDescending() |
[8, 5, 3, 3, 1] |
| 項目で昇順 | words.sortedBy { it.length } |
[apple, banana, cherry, avocado] |
| 項目で降順 | words.sortedByDescending { it.length } |
[avocado, banana, cherry, apple] |
| 複数キーで並べる | words.sortedWith(compareBy({ it.length }, { it })) |
[apple, banana, cherry, avocado] |
| 逆順 | nums.reversed() |
[1, 3, 8, 3, 5] |
グループ化・Map にする(groupBy・associate 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| キーごとにまとめる | items.groupBy { it.category }.mapValues { e -> e.value.map { it.name } } |
{文具=[pen, note], 食器=[cup, dish]} |
| まとめる値も指定 | items.groupBy({ it.category }, { it.price }) |
{文具=[120, 300], 食器=[500, 800]} |
| キーごとの件数 | items.groupingBy { it.category }.eachCount() |
{文具=2, 食器=2} |
| キーで引ける Map にする | items.associateBy { it.name }["cup"] |
Item(name=cup, category=食器, price=500) |
| キーと値を両方指定 | items.associate { it.name to it.price } |
{pen=120, note=300, cup=500, dish=800} |
| 要素をキー、計算結果を値に | words.associateWith { it.length } |
{apple=5, banana=6, cherry=6, avocado=7} |
| 組のリストを Map に | listOf("a" to 1, "b" to 2).toMap() |
{a=1, b=2} |
分割・結合・集合演算(chunked・windowed 系)
| やりたいこと | 書き方 | 出力 |
|---|---|---|
| n 件ずつに分ける | nums.chunked(2) |
[[5, 3], [8, 3], [1]] |
| 分けて各かたまりを集計 | nums.chunked(2) { it.sum() } |
[8, 11, 1] |
| 1つずつずらした窓 | nums.windowed(3) |
[[5, 3, 8], [3, 8, 3], [8, 3, 1]] |
| 幅と歩幅を指定した窓 | nums.windowed(3, step = 2, partialWindows = true) |
[[5, 3, 8], [8, 3, 1], [1]] |
| 要素を足した新しいリスト | nums + 9 |
[5, 3, 8, 3, 1, 9] |
| 要素を1件除いた新しいリスト | nums - 3 |
[5, 8, 3, 1] |
| リストどうしを連結 | nums + listOf(9, 10) |
[5, 3, 8, 3, 1, 9, 10] |
| 和集合 | listOf(1, 2, 3) union listOf(3, 4) |
[1, 2, 3, 4] |
| 共通部分 | listOf(1, 2, 3) intersect listOf(3, 4) |
[3] |
| 差集合 | listOf(1, 2, 3) subtract listOf(3, 4) |
[1, 2] |
| Set に変換 | nums.toSet() |
[5, 3, 8, 1] |
一覧の読み方と、似た関数の使い分け
| 似た関数 | 違い |
|---|---|
map と flatMap |
map は1要素→1要素。flatMap は1要素→リストにして、全部を1本につなげる |
filter と partition |
filter は合う側だけ。partition は合う側・合わない側の両方を Pair で返す |
fold と reduce |
fold は初期値を渡す(空でも初期値が返る)。reduce は先頭が初期値(空だと例外) |
groupBy と associateBy |
groupBy は同じキーの要素をリストにまとめる。associateBy は1キー1要素(同じキーはあとの要素が残る) |
associateBy・associateWith・associate |
順に「キーを計算」「値を計算」「キーと値の両方を計算」 |
chunked と windowed |
chunked は重ならない区切り。windowed は1つずつ(または step ずつ)ずらした重なる窓 |
sortedBy と sortedWith |
sortedBy は1つのキー。sortedWith は compareBy で複数キーを指定できる |
first() の NoSuchElementException と firstOrNull() の対比
空のリストに first() を呼ぶと、実行時に止まります。
fun main() {
val empty = emptyList<Int>()
println(empty.firstOrNull())
println(empty.firstOrNull() ?: -1)
println(empty.first())
}
null
-1
Exception in thread "main" java.util.NoSuchElementException: List is empty.
at kotlin.collections.CollectionsKt___CollectionsKt.first(_Collections.kt:222)
at FirstKt.main(first.kt:5)
at FirstKt.main(first.kt)
firstOrNull() は null を返すので、?: -1 で既定値を付けられます。3行目の first() で NoSuchElementException(List is empty.)になりました。
「〜OrNull」が付く関数は、該当がないときに例外ではなく null を返します。 対になる関数を同じように動かして、止まるときのメッセージを表にまとめました。
| 例外で止まる版 | null を返す版 | 止まるときのメッセージ |
|---|---|---|
first() / last() |
firstOrNull() / lastOrNull() |
List is empty. |
first { 条件 } |
firstOrNull { 条件 } / find { 条件 } |
Collection contains no element matching the predicate. |
single() |
singleOrNull() |
2件以上:List has more than one element. |
list[5] / elementAt(5) |
getOrNull(5) |
Index 5 out of bounds for length 3 |
max() / min() |
maxOrNull() / minOrNull() |
NoSuchElementException |
reduce { ... } |
reduceOrNull { ... } |
Empty collection can't be reduced. |
map.getValue(key) |
map[key] |
Key b is missing in the map. |
空のリストでも止まらないものもあります。fold(0) { ... } は初期値の 0、sum() は 0、average() は NaN を返しました。
使い分けの目安は、「空や該当なしが起こりうるなら OrNull 版と ?:」「空であるはずがなく、空なら誤りとして気付きたいなら例外版」です。
Sequence との評価順の違い
List に map や filter をつなぐと、1段ごとに全要素を処理して中間のリストを作ります。asSequence() で Sequence に変えると、1要素ずつ最後の段まで流す動きになります。println を入れて順番を確かめます。
fun main() {
val nums = listOf(1, 2, 3, 4, 5)
println("--- List(1段ずつ全件処理)")
val a = nums
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println("結果: $a")
println("--- Sequence(1要素ずつ最後まで流す)")
val b = nums.asSequence()
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println("結果: $b")
println("--- 終端操作がないと何も動かない")
val s = nums.asSequence().map { println(" map $it"); it * 2 }
println("型: ${s::class.simpleName}")
println(s.toList())
println("--- 無限の列から必要な分だけ")
println(generateSequence(1) { it * 2 }.take(10).toList())
println(sequence { yield(1); yieldAll(listOf(2, 3)) }.toList())
}
実行結果:
--- List(1段ずつ全件処理)
map 1
map 2
map 3
map 4
map 5
filter 2
filter 4
filter 6
filter 8
filter 10
結果: 6
--- Sequence(1要素ずつ最後まで流す)
map 1
filter 2
map 2
filter 4
map 3
filter 6
結果: 6
--- 終端操作がないと何も動かない
型: TransformingSequence
map 1
map 2
map 3
map 4
map 5
[2, 4, 6, 8, 10]
--- 無限の列から必要な分だけ
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
[1, 2, 3]
| 比べる点 | List(Iterable) | Sequence |
|---|---|---|
| 処理の順番 | 段ごと(map を5回 → filter を5回) | 要素ごと(map→filter を1要素ずつ) |
| 上の例での呼び出し回数 | map 5回+filter 5回 | map 3回+filter 3回 |
| 中間のリスト | 1段ごとに作る | 作らない |
| いつ動くか | 書いた行ですぐ | toList()・first()・sum() などの終端操作を呼んだとき |
| 無限に続く列 | 扱えない | generateSequence と take で必要な分だけ取れる |
Sequence では、3つ目の要素で条件に合う値(6)が見つかった時点で処理が終わり、4つ目と5つ目は map も呼ばれていません。また、終端操作を呼ぶまでは何も実行されないことも、出力の「型: TransformingSequence」のあとに初めて map が動いている点から分かります。
公式ドキュメントでは、Sequence は中間結果を作らないので大きなコレクションや段数の多い処理で有利になる一方、遅延のための手間が加わるので、小さなコレクションや単純な計算では Iterable のほうが合う場合がある、と説明されています。
配列との違い・等価比較・ループの書き方
fun main() {
val arr = arrayOf(1, 2, 3)
val ia = intArrayOf(1, 2, 3)
arr[0] = 9 // 配列は要素を書き換えられる(長さは固定)
println(arr.contentToString())
println(arr.toList())
println(listOf(1, 2).toTypedArray().contentToString())
println(ia.map { it * 2 }) // 結果は List
println(arrayOf(1, 2) == arrayOf(1, 2))
println(listOf(1, 2) == listOf(1, 2))
println(listOf(1, 2) == mutableListOf(1, 2))
println(setOf(1, 2) == setOf(2, 1))
println(mapOf("a" to 1, "b" to 2) == mapOf("b" to 2, "a" to 1))
// インデックス付きループ
val xs = listOf("a", "b")
for ((i, v) in xs.withIndex()) print("$i:$v ")
xs.forEachIndexed { i, v -> print("$i=$v ") }
for (i in xs.indices) print("${xs[i]} ")
println()
println("${xs.size} ${xs.lastIndex} ${xs.indices}")
}
実行結果(1〜3行目はコンパイル時の警告):
arrs.kt:9:27: warning: '==' on arrays compares only references. Replace '==' with 'contentEquals' to compare the arrays' contents or use '===' to remove the warning.
println(arrayOf(1, 2) == arrayOf(1, 2))
^^
[9, 2, 3]
[9, 2, 3]
[1, 2]
[2, 4, 6]
false
true
true
true
true
0:a 1:b 0=a 1=b a b
2 1 0..1
- 配列(Array)は長さ固定で、要素の書き換えはできます。 List とは
toList()/toTypedArray()で行き来できます。 - 配列の
==は参照の比較です。コンパイラも「contentEquals を使いましょう」と警告を出しています。結果は false でした。 - List・Set・Map の
==は中身の比較です。listOf(1, 2) == mutableListOf(1, 2)は true、Set と Map は順番が違っても true でした。 - ループは3通り:位置と値の両方がほしいなら
withIndex()かforEachIndexed、位置だけならindicesです。lastIndexはsize - 1と同じ値です。
どれを選ぶか:場面別の判断表
| 場面 | 選ぶもの |
|---|---|
| 中身を変えない一覧 | listOf / mapOf |
| あとから追加・削除する | mutableListOf / mutableMapOf |
| 条件つきで組み立てて、できあがりは変えない | buildList { } / buildMap { } |
| 関数の戻り値・引数の型 | List / Map(読み取り専用の型で受け渡す) |
| 空を返す | emptyList() / emptyMap() |
| 重複を持たせない | setOf / mutableSetOf |
| キー順に並べたい | sortedMapOf / toSortedMap() |
| 該当なしが起こりうる取り出し | firstOrNull / getOrNull / map[key] + ?: |
| 件数が多く段数も多い処理 | asSequence() |
よくある質問
Q. listOf と mutableListOf はどちらを使えばよいですか?
A. 追加・削除・置き換えをしないなら listOf、するなら mutableListOf です。読み取り専用にしておくと、add を書いた時点でコンパイルエラーになるので、意図しない変更に早く気付けます。要素を足した結果がほしいだけなら、list + 要素 で新しいリストを作る方法もあります。
Q. mutableListOf と arrayListOf の違いは何ですか?
A. 実体はどちらも java.util.ArrayList で、動きは同じです。違いは変数の型で、mutableListOf は MutableList、arrayListOf は ArrayList になります。ArrayList 型を求める API に渡す場合を除き、mutableListOf で足ります。
Q. Map を値の順に並べ替えるにはどうしますか?
A. stock.entries.sortedByDescending { it.value } で並べ替えた一覧が得られます。キーだけなら .map { it.key } を続けます。キー順でよければ toSortedMap() が使えます。
まとめ
listOf/mapOfは読み取り専用、mutableListOf/mutableMapOfは変更できる。迷ったら読み取り専用から始めるmutableListOfとarrayListOfの実体はどちらも ArrayList。mapOfとmutableMapOfは LinkedHashMap で入れた順を保つ- 読み取り専用 List への add はコンパイルエラー。
mutableListOfにするか、+やtoMutableList()で新しいリストを作る - 操作関数はもとのリストを変えずに新しい結果を返すので、filter → sortedBy → map とつないで書ける
first()は空だと NoSuchElementException、firstOrNull()は null。「〜OrNull」と?:の組み合わせが便利groupByはキーごとのリスト、associateByは1キー1要素の Map。foldは初期値あり、reduceは先頭が初期値- Sequence は1要素ずつ最後まで流し、終端操作で動き出す。件数や段数が多い処理、途中で打ち切れる処理に合う
参考・出典
- Kotlin 公式ドキュメント「Collections overview」 https://kotlinlang.org/docs/collections-overview.html
- Kotlin 公式ドキュメント「Sequences」 https://kotlinlang.org/docs/sequences.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin List・Mapの使い方とコレクション操作一覧【早見表】」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。