Kotlinのコレクション操作Tips その1

こんにちは。株式会社 Interfamilia の Waka です。
久しぶりの更新です。最近ブログをあまり書けていませんでした😭
弊社はメンバーが増え、毎日和気あいあいと楽しく開発業務を行っています💪

今回の記事では、自分が日々の開発業務で得た、頭の片隅にあると嬉しいかもしれない
Kotlinのコレクション操作に関するTIPSをご紹介したいと思います!

※なお、下記Tipsは Kotlin 1.9.25 で動作確認を行っています。


目次

Kotlinのコレクション操作Tips

指定サイズのListを作成

テストコード作成時など、中身は何でも良いから、複数の要素を持つListを作りたい!という場面があると思います。
以下の記述で、指定サイズのListを作成できます。

fun main() {
    val num = 3
    val list: List<String> = List(num) { "繰り返し" }
    
    println(list) // [繰り返し, 繰り返し, 繰り返し]
}

コンストラクタの引数で数量を指定。ラムダ式 {} でListの中身を設定しています。
MutableList.add() をループさせて作る方法もありますが、こちらの方がシンプルに書けますね。


コレクション同士の加算/減算

kotlinでは、コレクション同士の加算/減算が可能です。

n Kotlin, plus (+) and minus (-) operators are defined for collections. They take a collection as the first operand; the second operand can be either an element or another collection.
https://kotlinlang.org/docs/collection-plus-minus.html

加算は直感的です。2つのコレクションを合算して、両方の要素を持った和集合を生成します。
plus()メソッドを使います。+演算子でも実行可能です1

減算は少々特殊です。引かれた方は重複する要素だけが削減され、重複しなかった要素だけが後に残ります。
minus()メソッドを使います。-演算子でも実行可能です2

以下、+/-演算子の組み合わせ例です。減算の結果、listAから重複要素 (2, 3) が除去されています。

fun main() {
  val listA = listOf(1, 2, 3)
  val listB = listOf(2, 4, 5)
  val listC = listOf(3, 6, 7)

  val result = listA - (listB + listC) // [1, 2, 3] - [2, 3, 4, 5, 6, 7]
  print(result) // [1]
}

余談ですが…Kotlin1.6以降は減算する際 List - Set (引く側のコレクションをSetにする) とした方がパフォーマンスが良いそうです。
IntelliJ等のIDEを使っていると、以下のような補足表示が出ます。

List - Set とした方がパフォーマンスが良い
List - Set とした方がパフォーマンスが良い

これは、Kotlin1.6でminus()の実装が変わったためだそうです。以下の記事が詳しいです。


空配列のflatten

Kotlinには、ネストしたコレクションをフラットなListに変換するflatten()メソッドがあります。

以下、使用例となります。
ネストした複数のListから要素を取り出して、新規のListを生成しています。

fun main() {
    val list = listOf(listOf(1), listOf(2), listOf(3), listOf(3, 4))
    val flattenSet = list.flatten().toSet()
    println(flattenSet) // [1, 2, 3, 4]
}

ここで豆知識です。
空配列をネストしたコレクションに対してflatten()を実行すると、何も残らないという特性があります。

以下、一例となります。

fun main() {
    val list: List<List<Int>> = listOf(emptyList())
    println(list.flatten()) // []
}

groupBy()でList内の重複してる要素を取り出す

Kotlinには、コレクションを条件指定でグループ化するgroupBy()メソッドがあります。

このgroupBy()を利用することで、List内の重複した要素を抽出することが可能です。
以下、一例となります。

fun main() {
    val list = listOf(1, 2, 3, 3, 4, 5, 5, 5) // 3 と 5 が重複している
    
    val firstDuplicatedNumber = list.groupBy { it }.filterValues { it.size > 1 }.keys
    
    print(firstDuplicatedNumber) // [3, 5]
}

まず list.groupBy { it } で、各要素をkeyとしたMapを生成します。
この時、重複した値がある場合、同じkeyでまとめられます

{1=[1], 2=[2], 3=[3, 3], 4=[4], 5=[5, 5, 5]}

このMapに対して filterValues { it.size > 1 } を行うと、valueの要素数が2以上のもの (= 重複した値) を抽出できます。

{3=[3, 3], 5=[5, 5, 5]}

最後に、このMapのkeyを取り出してlist化すれば、重複した値の一覧が取り出せます。

[3, 5]

associate系メソッドの注意点

コレクションからMapを生成したい時は、associate系メソッドが便利です。
以下のバリエーションがあります。

以下、使用例になります。

fun main() {
    val item1 = Item(1, "タウリン")
    val item2 = Item(2, "ブロムヘキシン")
    val item3 = Item(3, "リゾチウム")
    val itemList = listOf(item1, item2, item3)
    
    // associate - key, valueを指定できる
    println(itemList.associate { it.id to it.name }) // { 1=タウリン, 2=ブロムヘキシン, 3=リゾチウム }
    
    // associateBy - keyを指定できる
    println(itemList.associateBy { it.id }) // { 1=item1, 2=item2, 3=item3 }
    
    // associateWith - valueを指定できる
    println(itemList.associateWith { it.name }) // { item1=タウリン, item2=ブロムヘキシン, item3=リゾチウム }
}

data class Item(
    val id: Int,
    val name: String,
)

気を付けなければならないのが、keyが重複する場合です。
associate()/associateBy()でMapを生成する時、keyの値が重複する場合は、元となるコレクションの最後にある要素が採用されるようです。

以下、keyが重複する場合の例です。

fun main() {
  val test1 = TestClass("sameKey", 1)
  val test2 = TestClass("sameKey", 50)
  val test3 = TestClass("sameKey", 100)

  val list = listOf(test1, test3, test2)
  list.associateBy { it.keyString }.forEach{ println(it.value.valueNumber) } // 50 (=test2)
  
  val ascendingList = list.sortedBy{ it.valueNumber } // test3が最後
  ascendingList.associateBy { it.keyString }.forEach{ println(it.value.valueNumber) } // 100 (=test3)
  
  val descendingList = list.sortedByDescending{ it.valueNumber } // test1が最後
  descendingList.associateBy { it.keyString }.forEach{ println(it.value.valueNumber) } // 1 (=test1)
}

data class TestClass(
  val keyString: String,
  val valueNumber: Int,
)

このためassociate()/associateBy()を使う際は、 あらかじめSetでkey重複を排除するなどの工夫が必要です。


Mapのkeyにコレクションを指定する

associate()系メソッドでMapを生成する際、keyにコレクションを指定することも可能です。

以下、具体例です。

fun main() {
  val monster1 = Monster(
    level = 1,
    skills = listOf("ひっかく", "なきごえ"),
  )
  val monster2 = Monster(
    level = 2,
    skills = listOf("たいあたり", "しっぽをふる"),
  )
  val monster3 = Monster(
    level = 3,
    skills = listOf("ひっかく", "なきごえ"), // skillsの構成要素はmonster1と同じ
  )

  val monsters = listOf(monster1, monster2, monster3)
  println(monsters.size) // size=3

  val map = monsters.associateBy { it.skills } // List<String>をkeyに指定
  println(map.size) // size=2
  map.forEach {
    print("key=" + it.key + ", ")
    println("value=" + it.value)
  }
  // key=[ひっかく, なきごえ], value=Monster(level=3, skills=[ひっかく, なきごえ])
  // key=[たいあたり, しっぽをふる], value=Monster(level=2, skills=[たいあたり, しっぽをふる])
}

data class Monster(
  val level: Int,
  val skills: List<String>,
)

元となるコレクションmonsters は3つのMonsterクラスを持っています。
これをskillsをkeyにMap変換すると、Mapのサイズは2になります。

monster1, monster3は全く同じskills listOf("ひっかく", "なきごえ") を持っています。
associateByでMap変換する際、前述の「keyの値が重複する場合、元となるコレクションの最後の要素が採用される」特性により
monster3が採用され、結果としてMapのサイズが2に減ってしまったのです。

Map生成する際にはkeyの値が重複しないよう注意しましょう。


最後に

以上、Kotlinのコレクション操作に関するTips紹介でした。
今後も面白い発見などあれば、都度ブログ投稿していければと考えています!

それではまた次回!


参考リンク


会社を一緒に盛り上げてくれる仲間を募集しています!

応募フォームには、外部サービスengageを利用しています。
ご応募に際して、何かわからないことなどございましたら こちら からお気軽にお問い合わせください。


  1. +演算子は省略記法で、内部でplus()を呼び出しています。 ↩︎

  2. -演算子も同様に、内部でminus()を呼び出しています。 ↩︎