0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Kotlin] コレクション操作チートシート Part. 1

0
Posted at

はじめに

おはようございます。
最近Kotlinを触っているのですが、コレクション(List<T>とかMap<T>とかですね)操作が思ったより色々あったので、自分なりにまとめてみることにしました。
もしよければご活用ください。

実行環境

Java 25, Kotlin Script (.kts) として実行しています。

kotlinc -script sample.main.kts

みたいな感じです。

サンプルデータ

Claudeに書いてもらいました。

data.kts
// ===== データクラス定義 =====
 
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が帰ってきたものだけを集めます。

sample.main.kts
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が帰ってきた最初の要素を返します。

sample.main.kts
val found = items.find { it.price >= 10000 }
println(found)
Item(id=2, name=メカニカルキーボード, price=15000, category=GADGET)

findLastを使うと、条件に一致する最後の要素を取得することができます。

sample.main.kts
val found = items.findLast { it.price >= 10000 }
println(found)
Item(id=5, name=ノイズキャンセリングイヤホン, price=25000, category=GADGET)

first() / last(): 最初または最後の要素を取得する

名前通りです。firstで最初の要素が、lastで最後の要素が返されます。

sample.main.kts
val first = items.first()
val last = items.last()
println(first.name)
println(last.name)
Kotlinの教科書
実践Kotlin

map(): 要素を変換する

雑な言い方をすれば「変換」です。
軽く語源を調べたところ、数学の概念である「写像 (mapping)」に由来するみたいです。
ある集合の要素を別の集合の要素にマッピング(再割り当て)するという操作ですね。

各アイテムを受け取って、任意の型を返すラムダ式を引数に指定します。
filterとは異なり、返したオブジェクトがそのままリストの要素になります。

sample.main.kts
// users の各要素を it.name に変換
val mapped = users.map { it.name }
println(mapped)
[Alice, Bob, Carol, Dave, Eve]

filterと組み合わせると、usersから名前の長さが4文字以上のUserのメールアドレスを取り出す、みたいなことができます。

sample.main.kts
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同様、各アイテムを受け取り、任意の型を返すラムダ式を指定します。
ラムダ式で返された値をもとにグルーピングが行われます。

sample.main.kts
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を元にグルーピングしています。

sample.main.kts
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つの用法があります。引数無しで実行した場合、要素の数を返します。

sample.main.kts
val count = items.count()
println(count)
6

引数としてBooleanを返すラムダ式を指定すると、trueが返される要素の数を返します。
filterと似てますね。

sample.main.kts
val over10kPriceCount = items.count { it.price >= 10000 }
println(over10kPriceCount)
2

sizeと何が違うのという話ですが、どうやら計算量が違うらしいです。
sizeはO(1)、countはO(N)とのこと。
...が、どうやら確認したところ、引数無しでcount()を呼び出した場合
どうやらそのままsizeが返されるだけっぽいので、わざわざ意識する必要はないのかもしれません。
引数ありで、条件に合致する要素の個数を取る場合は当然O(N)です。

sort() / sorted(): 並び変える

要素が比較可能な場合、sort()でコレクションを並び変えることができます。

sample.main.kts
val numbers = mutableListOf(6, 2, 4, 1, 5)
numbers.sort()
println(numbers)
[1, 2, 4, 5, 6]

オブジェクトなど、直接比較ができない場合、sortByを使用して比較基準を設定できます。

sample.main.kts
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を使うことで並び変えられた新しいリストを得ることができます。

sample.main.kts
val numbers = listOf(6, 2, 4, 1, 5)
val sorted = numbers.sorted()
println(numbers[0])
println(sorted[0])
6
1

同様に、sortedByを使用することで、sortByのように比較基準を設定できます。

sample.main.kts
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を返すラムダ式を指定します。

sample.main.kts
val isAllAdmin = users.all { it.isAdmin }
println(isAllAdmin)
false

ちなみに、これを書いてる段階で気になったのですが、リストの途中でfalseが返されるとどうなるのでしょうか?
全ての要素が条件を満たされたらtrueを返すのであれば、裏を返せば、一つでもfalseが帰ってきたらその時点でfalseを返せますね。
テストしてみましょう。

sample.main.kts
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を返します。

sample.main.kts
val a = emptyList<Int>()
val b = listOf(1, 2, 3)

println("a: ${a.any()}, b: ${b.any()}")
a: false, b: true

引数(Booleanを返すラムダ式)を指定すると、少なくとも一つの要素がある条件を満たすかどうかを判断します。

sample.main.kts
val hasOver10kPrice = items.any { it.price >= 10000 }
println(hasOver10kPrice)
true

all同様、一つでも条件に当てはまればtrueが返されます。

sample.main.kts
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(): 要素が含まれているかどうかを確認する

sample.main.kts
val firstOrder = orders[0]
val kotlinBook = items[0]

val kotlinBookOrdered = firstOrder.items.contains(kotlinBook)
println(kotlinBookOrdered)
true

お仲間として、containsAllがあります。
containsAllはcontainsとは異なり、引数にCollectionを受け取ります。
全ての要素が含まれている場合、trueが返されます。

sample.main.kts
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!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?