Kotlinのコレクション操作Tips その1
こんにちは。株式会社 Interfamilia の Waka です。
久しぶりの更新です。最近ブログをあまり書けていませんでした😭
弊社はメンバーが増え、毎日和気あいあいと楽しく開発業務を行っています💪
今回の記事では、自分が日々の開発業務で得た、頭の片隅にあると嬉しいかもしれない
Kotlinのコレクション操作に関するTIPSをご紹介したいと思います!
※なお、下記Tipsは Kotlin 1.9.25 で動作確認を行っています。
目次
テストコード作成時など、中身は何でも良いから、複数の要素を持つ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を使っていると、以下のような補足表示が出ます。
これは、Kotlin1.6でminus()
の実装が変わったためだそうです。以下の記事が詳しいです。
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()) // []
}
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]
コレクションから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重複を排除するなどの工夫が必要です。
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を利用しています。
ご応募に際して、何かわからないことなどございましたら こちら
からお気軽にお問い合わせください。