KotlinのCollection(List)の話
Sansan Advent Calendar 2019 の 16日目の記事です。
最近 Kotlin を勉強中でコレクションの扱いについて調べていたのですが、その中でも基本の List に絡めて書こうと思います。
List とは?
リファレンスには以下のように書かれています。
List is an ordered collection with access to elements by indices — integer numbers that reflect their position. Elements can occur more than once in a list. An example of a list is a sentence: it’s a group of words, their order is important, and they can repeat.
インデックスで要素にアクセスできる順序を持ったコレクションですね。
2つの List
List には List<T>
と MutableList<T>
があります。
2つの違いは生成されたあとに要素を変更できるかどうかです。
それぞれの定義を見てみます。
// List<T>
public interface List<out E> : Collection<E> {
...
}// MutableList<T>
public interface MutableList<E> : List<E>, MutableCollection<E> {
...
}
List<T>
は Collection<T>
から派生しているのに対して MutableList<T>
は List<T>
、MutableCollection<T>
から派生しています。
この MutableCollection<T>
が add
や remove
などの要素を操作するメソッドを持っています。
MutableList<T> の注意点
先にみた通り MutableList<T>
は List<T>
からも派生しています。
なので List<T>
として扱うことができてしまい、そうした場合には読み取り専用が保証されなくなるので注意が必要です。
fun liar() {
val mutableList: MutableList<Int> = mutableListOf(1, 2, 3)
// List<T> として扱える
val list: List<Int> = mutableList
// MutableList<T> の要素を clear
mutableList.clear()
// List<T> を列挙しても何も表示されない!
for (x in list) {
println(x)
}
}
要素が変更されないから安心だと思ったら実は…と思わぬ不具合を生む可能性があるので気をつけたいですね。
定義をもう少し追ってみる
それぞれの List の定義を追っていくと最終的に Iterable<T>
に行き着きます。
定義を見てみます。
public interface Iterable<out T> {
public operator fun iterator(): Iterator<T>
}
Iterator<T>
を取得する iterator
メソッドだけを持っているだけです。Iterator<T>
の定義も見てみます。
public interface Iterator<out T> {
public operator fun next(): T
public operator fun hasNext(): Boolean
}
こちらもシンプルですね。
しかしこの Iterable<T>
と Iterator<T>
はコレクションを扱うのにとても重要な役割を果たします。
そう!コレクションの要素を列挙する機能を提供します!
コレクションを列挙する
for
ループで列挙させてみます。
val list = listOf(1, 2, 3)// 1, 2, 3 が出力される
for (x in list) {
println(x)
}
この for
はシンタックスシュガーで展開されると以下のようなコードが生成されます。
val list = listOf(1, 2, 3)
val iterator = list.iterator()
while(iterator.hasNext()) {
val x = iterator.next()
println(x)
}
Iterator<T>
を使ったループ処理ですね。
実は for
で列挙したいだけなら Iterable<T>
は必要なく、 iterator
メソッドを実装するだけで事足ります。
class MyCollection<T> {
private val list: MutableList<T> = mutableListOf()
fun add(item: T) {
list.add(item)
}
operator fun iterator() : Iterator<T> {
return list.iterator()
}
}fun main() {
val myCollection = MyCollection<Int>()
myCollection.add(100)
myCollection.add(200)
for (x in myCollection) {
println(x)
}
}
では Iterable<T>
が必要な場面はどんなときでしょうか。
コレクションを操作する
Kotlin ではコレクションを操作するためのmap
や filter
といった便利なメソッド群が定義されています。
これらは Iterable<T>
の拡張メソッドとして定義されているのでこれらを利用したい場合には Iterable<T>
から派生する必要があります。
これまで見てきた List は Iterable<T>
から派生しているのでこれらのメソッド群を利用することができます。
val list: List<Int> = listOf(1, 2, 3, 4)
val operatedList = list
.filter { x -> x % 2 == 0 }
.map { x -> x * 2 }
// 4, 8 が出力される
for (x in operatedList) {
println(x)
}
便利ですね!
例として filter
の実装を見てみます。
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
難しいことはしておらず for
で列挙して条件に一致する要素を新しい List に詰め直しているだけです。
コレクション操作のメソッド群は inline
で定義されているので呼び出し元にコードが展開されます。
filter
はどのように展開されるでしょうか。
// 変換前
fun main() {
val list: List<Int> = listOf(1, 2, 3, 4)
val operatedList = list
.filter { x -> x % 2 == 0 }
}// 変換後
fun main() {
val list = listOf(1, 2, 3, 4)
val iterable: Iterable<Int> = list
val destination = mutableListOf<Int>()
val iterator = iterable.iterator()
while(iterator.hasNext()) {
val x = iterator.next()
if (x % 2 == 0) {
destination.add(x)
}
}
val operatedList: List<Int> = destination;
}
一行だったコードが一気に増えました。
実際にはもっと機械的なコードが生成されますが、読みにくいので成形してあります。
普段何気なく使っているコレクションですが実装を探索すると結構いろんなことをやっています。
簡単に使うことができるようにコンパイラががんばってくれていたり。
実装を知ると考え方が広がると思うのでどうやって動いてるんだろう?と疑問に思ったコードはどんどんデコンパイルしてみてはいかがでしょうか。
(Intellij IDEA では Tools -> Kotlin -> Show Kotlin Bytecode からデコンパイルすることができます。)