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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?