はじめに
おはようございます。
最近Kotlinを触っているのですが、コレクション(List<T>とかMap<T>とかですね)操作が思ったより色々あったので、自分なりにまとめてみることにしました。
もしよければご活用ください。
実行環境
Java 25, Kotlin Script (.kts) として実行しています。
kotlinc -script sample.main.kts
みたいな感じです。
サンプルデータ
Claudeに書いてもらいました。
// ===== データクラス定義 =====
data class Item(
val id: Int,
val name: String,
val price: Int,
val category: String,
)
data class Order(
val id: Int,
val userId: Int,
val items: List<Item>,
val status: String, // "PENDING", "SHIPPED", "DELIVERED", "CANCELLED"
)
data class User(
val id: Int,
val name: String,
val email: String,
val isAdmin: Boolean,
val department: String,
)
// ===== サンプルデータ =====
val items = listOf(
Item(1, "Kotlinの教科書", 3000, "BOOK"),
Item(2, "メカニカルキーボード", 15000, "GADGET"),
Item(3, "Clean Architecture", 4000, "BOOK"),
Item(4, "USBハブ", 3500, "GADGET"),
Item(5, "ノイズキャンセリングイヤホン", 25000, "GADGET"),
Item(6, "実践Kotlin", 3500, "BOOK"),
)
val users = listOf(
User(1, "Alice", "alice@example.com", isAdmin = true, department = "Engineering"),
User(2, "Bob", "bob@example.com", isAdmin = false, department = "Engineering"),
User(3, "Carol", "carol@example.com", isAdmin = false, department = "Sales"),
User(4, "Dave", "dave@example.com", isAdmin = true, department = "Sales"),
User(5, "Eve", "eve@example.com", isAdmin = false, department = "HR"),
)
val orders = listOf(
Order(101, userId = 1, items = listOf(items[0], items[2]), status = "DELIVERED"),
Order(102, userId = 2, items = listOf(items[1], items[3]), status = "SHIPPED"),
Order(103, userId = 1, items = listOf(items[4]), status = "PENDING"),
Order(104, userId = 3, items = listOf(items[0], items[5]), status = "DELIVERED"),
Order(105, userId = 4, items = listOf(items[2], items[3]), status = "CANCELLED"),
Order(106, userId = 5, items = listOf(items[1], items[4]), status = "PENDING"),
)
基本形
体感ですが、使用頻度が高いので抑えておきたいものです。
filter(): 条件に一致するものだけに絞り込む
リストの中から、条件に合うものを探します。データベースに馴染み深いかたなら、WHERE句という表現がしっくりくるかも。
各アイテムを受け取って、Booleanを返すラムダ式を引数に指定します。trueが帰ってきたものだけを集めます。
val filtered = items.filter { it.price >= 10000 } // itは各アイテムを指します
println(filtered)
[Item(id=2, name=メカニカルキーボード, price=15000, category=GADGET), Item(id=5, name=ノイズキャンセリングイヤホン, price=25000, category=GADGET)]
filterに限りませんが、当然ながらこのラムダ式は毎回実行されます。例えば、
items.filter { println(it.price); it.price >= 10000 }
としてみると、各Itemのpriceが表示されます。
3000
15000
4000
3500
25000
3500
これはラムダ式全般に言えることですが、itの名前が気に食わなければ、
items.filter { item -> item.price >= 10000 }
とすることができます。
find(): 条件に一致する最初の要素を取得する
filter()の親戚と言えます。Booleanを返すラムダ式を受け取り、trueが帰ってきた最初の要素を返します。
val found = items.find { it.price >= 10000 }
println(found)
Item(id=2, name=メカニカルキーボード, price=15000, category=GADGET)
findLastを使うと、条件に一致する最後の要素を取得することができます。
val found = items.findLast { it.price >= 10000 }
println(found)
Item(id=5, name=ノイズキャンセリングイヤホン, price=25000, category=GADGET)
first() / last(): 最初または最後の要素を取得する
名前通りです。firstで最初の要素が、lastで最後の要素が返されます。
val first = items.first()
val last = items.last()
println(first.name)
println(last.name)
Kotlinの教科書
実践Kotlin
map(): 要素を変換する
雑な言い方をすれば「変換」です。
軽く語源を調べたところ、数学の概念である「写像 (mapping)」に由来するみたいです。
ある集合の要素を別の集合の要素にマッピング(再割り当て)するという操作ですね。
各アイテムを受け取って、任意の型を返すラムダ式を引数に指定します。
filterとは異なり、返したオブジェクトがそのままリストの要素になります。
// users の各要素を it.name に変換
val mapped = users.map { it.name }
println(mapped)
[Alice, Bob, Carol, Dave, Eve]
filterと組み合わせると、usersから名前の長さが4文字以上のUserのメールアドレスを取り出す、みたいなことができます。
val combined = users
// 名前の長さが4文字以上の`User`を取り出す
.filter { it.name.length >= 4 }
// itは`User`オブジェクト、it.emailを返すことでメールアドレスに変換できる
.map { it.email }
println(combined)
[alice@example.com, carol@example.com, dave@example.com]
groupBy(): 要素を分類する
リストをある要素で分類し、Mapにします。
map同様、各アイテムを受け取り、任意の型を返すラムダ式を指定します。
ラムダ式で返された値をもとにグルーピングが行われます。
val grouping = users.groupBy { it.department }
println(grouping)
(ちょっと見づらかったので整形してます)
{
Engineering=[User(id=1, name=Alice, email=alice@example.com, isAdmin=true, department=Engineering), User(id=2, name=Bob, email=bob@example.com, isAdmin=false, department=Engineering)],
Sales=[User(id=3, name=Carol, email=carol@example.com, isAdmin=false, department=Sales), User(id=4, name=Dave, email=dave@example.com, isAdmin=true, department=Sales)],
HR=[User(id=5, name=Eve, email=eve@example.com, isAdmin=false, department=HR)]
}
ここで初めてMapの概念が出てきたので軽く触っておこうと思うのですが、キーと値のペアみたいなもので、一部の言語では「辞書」「ハッシュマップ」または「連想配列」と呼ばれたりします。
すごく雑に言うと、リストはインデックス(数字)をキーとして値を取るのに対し、Mapは任意の値をキーとして値を取ることができます。
今回の例では、it.departmentを元にグルーピングしています。
val grouping = users.groupBy { it.department }
val sales = grouping["Sales"]
println(sales)
のようにすることで、"Sales" グループに属するユーザーを表示することができます。
[User(id=3, name=Carol, email=carol@example.com, isAdmin=false, department=Sales), User(id=4, name=Dave, email=dave@example.com, isAdmin=true, department=Sales)]
groupBy は、ラムダ式で返された値をもとにグルーピングを行うため、グループ基準がリストアイテムと直接関連している必要はありません。
そのため、多少極端な例ですが、以下のようにすることでリストを3グループに振り分けることもできます。
val grouping = users
.map { it.name }
.groupBy { Random.nextInt(0, 3) }
println(grouping)
{0=[Alice, Bob, Carol], 1=[Dave], 2=[Eve]}
count(): 要素の数を数える
countには2つの用法があります。引数無しで実行した場合、要素の数を返します。
val count = items.count()
println(count)
6
引数としてBooleanを返すラムダ式を指定すると、trueが返される要素の数を返します。
filterと似てますね。
val over10kPriceCount = items.count { it.price >= 10000 }
println(over10kPriceCount)
2
sizeと何が違うのという話ですが、どうやら計算量が違うらしいです。
sizeはO(1)、countはO(N)とのこと。
...が、どうやら確認したところ、引数無しでcount()を呼び出した場合
どうやらそのままsizeが返されるだけっぽいので、わざわざ意識する必要はないのかもしれません。
引数ありで、条件に合致する要素の個数を取る場合は当然O(N)です。
sort() / sorted(): 並び変える
要素が比較可能な場合、sort()でコレクションを並び変えることができます。
val numbers = mutableListOf(6, 2, 4, 1, 5)
numbers.sort()
println(numbers)
[1, 2, 4, 5, 6]
オブジェクトなど、直接比較ができない場合、sortByを使用して比較基準を設定できます。
data class MyItem(val name: String, val price: Int)
val items = mutableListOf(
MyItem("Apple", 100),
MyItem("Grape", 200),
MyItem("Banana", 150)
)
items.sortBy { it.price }
println(items)
[MyItem(name=Apple, price=100), MyItem(name=Banana, price=150), MyItem(name=Grape, price=200)]
なお、sortは破壊的操作です。つまり、ミュータブル(変更可能)なリストを直接操作します。
意図しないリストの操作はバグを招くことがあり、あまり推奨されることではありません。時と場合にもよりますが。
println(items[1].name)
items.sortBy { it.price }
println(items[1].name)
Grape
Banana
このようなケースの場合、sortedを使うことで並び変えられた新しいリストを得ることができます。
val numbers = listOf(6, 2, 4, 1, 5)
val sorted = numbers.sorted()
println(numbers[0])
println(sorted[0])
6
1
同様に、sortedByを使用することで、sortByのように比較基準を設定できます。
data class MyItem(val name: String, val price: Int)
val items = mutableListOf(
MyItem("Apple", 100),
MyItem("Grape", 200),
MyItem("Banana", 150)
)
val sortedItems = items.sortedBy { it.price }
println(items[1].name)
println(sortedItems[1].name)
Grape
Banana
特別なことがない場合は、sorted() / sortedBy() を使うのがいいと思います。
フィルタと条件分岐
コレクション操作ではありませんが、比較的便利なので。
all(): 全ての要素が条件を満たすか判断する
要素が全て、ある条件を満たすかどうかを判断します。
全ての要素が条件に合致すると、trueを返します。
引数には、条件としてBooleanを返すラムダ式を指定します。
val isAllAdmin = users.all { it.isAdmin }
println(isAllAdmin)
false
ちなみに、これを書いてる段階で気になったのですが、リストの途中でfalseが返されるとどうなるのでしょうか?
全ての要素が条件を満たされたらtrueを返すのであれば、裏を返せば、一つでもfalseが帰ってきたらその時点でfalseを返せますね。
テストしてみましょう。
val numbers = listOf(0, 1, 2, 3, 4, 5)
val isAllEven = numbers.all {
println("$it checked")
it % 2 == 0
}
println(isAllEven)
0 checked
1 checked
false
...なるほど。一つでもfalseが帰ってきた段階で、それ以降の要素は確認されずにfalseが返されるようですね。
any(): いずれかの要素が条件を満たすか判断する
allの親戚...かと思いきや、anyには2つの用法があります。
引数無しでanyを実行した場合、少なくとも一つの要素が含まれていた場合にtrueを返します。
val a = emptyList<Int>()
val b = listOf(1, 2, 3)
println("a: ${a.any()}, b: ${b.any()}")
a: false, b: true
引数(Booleanを返すラムダ式)を指定すると、少なくとも一つの要素がある条件を満たすかどうかを判断します。
val hasOver10kPrice = items.any { it.price >= 10000 }
println(hasOver10kPrice)
true
all同様、一つでも条件に当てはまればtrueが返されます。
val numbers = listOf(1, 2, 3, 4, 5)
val isAllEven = numbers.any {
println("$it checked")
it % 2 == 0
}
println(isAllEven)
1 checked
2 checked
true
contains(): 要素が含まれているかどうかを確認する
val firstOrder = orders[0]
val kotlinBook = items[0]
val kotlinBookOrdered = firstOrder.items.contains(kotlinBook)
println(kotlinBookOrdered)
true
お仲間として、containsAllがあります。
containsAllはcontainsとは異なり、引数にCollectionを受け取ります。
全ての要素が含まれている場合、trueが返されます。
val firstOrder = orders[0]
val kotlinBooks = items.filter { it.name.contains("Kotlin") }
// --> Kotlinの教科書, 実践Kotlin
val allKotlinBooksOrdered = firstOrder.items.containsAll(kotlinBooks)
println(allKotlinBooksOrdered)
false
おしまい
量が量なので、とりあえずこの辺で打ち止めにしたいと思います。多すぎる。
Part 2では、associate / partition / distinct あたりのちょいマニアックなやつに絞ってまとめてみようかなと。
私個人としても、Qiitaに書くことで良いアウトプットになったかと思います。では、cya!