PHONE APPLI Engineer blog

エンジニアブログ

Kotlin 紹介 - List / MutableList

弊社プロダクト「PHONE APPLI PEOPLE (旧:連絡とれるくん)」では、サーバサイドに Kotlin を採用しています。
今回は、以前から実態を調べたいと思っていた Kotlin の List / MutableList について紹介します。

List / MutableList とは

リスト機能のための interface です。
List はリストに対する読込機能があり、 MutableList は List の機能 + リストの書込機能があります。
両者とも kotlin.collections パッケージに置かれてます。

これだけの説明だとただの interface っぽいですが、実はこの 2 つ、 Java から見ると両方とも java.util.List 扱いされます。

え、何でこんなことしてるの? と最初は思いましたが、
Kotlin は Java との相互運用を重視しているので独自のリスト interface は作らなかった、でも Kotlin のコード上では読込専用リストの interface が欲しい、のような話になったのではないかと推測してます。

実行時はどうなるの?

List / MutableList ともに java.util.List 扱いされるのであれば、実行時の型情報はどうなるの?の疑問が出てくると思います。

結論から言うと、 Kotlin コード内で List / MutableList を継承して作ったクラスに対して is List<*> / is MutableList<*> チェックは有効です。

class ListImpl<E> : List<E> { /* 中身は省略 */ }

class MutableListImpl<E> : MutableList<E> { /* 中身は省略 */ }

@Suppress("USELESS_IS_CHECK")
fun main() {
    // 当然 true (USELESS_IS_CHECK を Suppress しないと警告になる)
    println(ListImpl<Int>() is List<*>)
    // List は MutableList を継承してないので false のはず
    println(ListImpl<Int>() is MutableList<*>)

    // MutableList は List を継承しているので true のはず
    println(MutableListImpl<Int>() is List<*>)
    // 当然 true (USELESS_IS_CHECK を Suppress しないと警告になる)
    println(MutableListImpl<Int>() is MutableList<*>)
}

このコードの実行結果は、期待どおり下記になります。

true
false
true
true

List / MutableList は実行時に存在しないのに、なぜこのコードが実現可能なのか?
の秘密は、 Kotlin バイトコードにあります。
(IntelliJ IDEA の場合、 Tools -> Kotlin -> Show Kotlin Bytecode で見れる奴)

  • is List<*>バイトコードINSTANCEOF java/util/List で、 Java の instanceof にあたると推測できます。

  • is MutableList<*>バイトコードINVOKESTATIC kotlin/jvm/internal/TypeIntrinsics.isMutableList (Ljava/lang/Object;)Z で、 kotlin.jvm.internal.TypeIntrinsics.isMutableList を実行しているっぽいことが分かります。
    つまり、 is MutableList<*> の実態は Kotlin が用意している関数になります。

TypeIntrinsics.isMutableList とは

コードは、 github で公開されており、下記のような感じです。

    public static boolean isMutableList(Object obj) {
        return obj instanceof List &&
               (!(obj instanceof KMappedMarker) || obj instanceof KMutableList);
    }

日本語にしてみると、下記条件を両方満たす場合 true を返す関数になっています。

  • obj が java.util.List を継承している
  • obj が KMappedMarker を継承してない、または、obj が KMutableList を継承している

KMappedMarker, KMutableList とは

パッケージ名 kotlin.jvm.internal.markers からするとマーカー interface です。

ここで最初のコードの ListImpl / MutableListImpl のバイトコードを見てみます

  • class ListImpl<E> : List<E>バイトコードpublic final class ListImpl implements java/util/List kotlin/jvm/internal/markers/KMappedMarker

  • class MutableListImpl<E> : MutableList<E>バイトコードpublic final class my/sandbox/MutableListImpl implements java/util/List kotlin/jvm/internal/markers/KMutableList

ListImpl / MutableListImpl 両者とも java.util.List の他に KMappedMarker / KMutableList を継承していることが分かります。
これらによって TypeIntrinsics.isMutableList の実装が意味のあるものになる、つまり is MutableList<*> を実現するためのマーカー interface と認識していいと思います。

ちなみに KMutableListKMappedMarker を継承してます。 継承関係は KMappedMarker -> KMutableIterable -> KMutableCollection -> KMutableList のようになってます。 (github ソース)

Kotlin では無い java.util.List 継承クラスは?

TypeIntrinsics.isMutableList の実装に「obj が KMappedMarker を継承してない、または、」条件が入っている理由は、 Kotlin でコンパイルしてない java.util.List を継承したクラスを MutableList 扱いするためだと推測してます。

例えば java.util.List を継承している java.util.ArrayList は Kotlin からすると MutableList 扱いしたいと思います。
java.util.ArrayList は Kotlin でコンパイルしたものでは無いので当然 KMappedMarker を継承してない、つまり TypeIntrinsics.isMutableList(arrayList) は true になり、 arrayList is MutableList<*> の結果も true になります。

注意したい点

Kotlin 1.4.30 で確認した内容です

標準関数で listOf というものがあります。
決まった要素数のリストを作る関数で、戻り値の型は Kotlin の List です。

これに対して is MutableList<*> を実行すると少し不可解なことが起こります。

fun main() {
    println(listOf<Int>() is MutableList<*>)
    println(listOf(1) is MutableList<*>)
    println(listOf(1, 2) is MutableList<*>)
}

// 結果
// false
// true
// true

この理由は listOf の引数の数によって、戻り値の実装クラスが異なるためです。

  • 0 個の場合、 kotlin.collections.EmptyList
  • 1 個の場合、 java.util.Collections$SingletonList
    java.util.Collections.singletonList で作られる
  • 2 個以上の場合、 java.util.Arrays$ArrayList
    java.util.Arrays.asList で作られる

kotlin.collections.EmptyList だけは Kotlin でコンパイルされて KMappedMarker を継承しているので MutableList とは判定されませんが、それ以外は Kotlin でコンパイルしてないので MutableList 扱いになってしまいます。

標準関数としてこの挙動は不親切だと思ってます。
List は MutableList である可能性もあるので 間違ってる とは言えないですが、気持ちとしては引数の数に関わらず Kotlin で実装した読込専用リストのクラスが返ってきてほしいところです。

KT-9659 で issue も報告されてますが、長い間放置されてるので優先度は低いのだろうと思います。
私の経験上、「is MutableList<*> で判定して true の場合にリストを書き換える」のような処理が必要になったことはまったく無いので、現実的には出番が少ない機能だろうと思います。

終わりに

Kotlin の List / MutableList について調べてみました。

Kotlin コンパイラが頑張って Java に無い機能 (読込専用リストの interface) を Java との相互運用を保ちつつ実現してるんだなあ・・・と。
読込専用リストの interface が Java にあれば一番いいと思いますが、 Java がそれを用意する気配は無さそうです。 (Collections.singletonXxxCollections.unmodifiableXxx で不変コレクションを作成する機能は既にありますが対応した interface は無いため)

ちなみに、他のコレクションインターフェースについても、実態は List / MutableList と似たような感じです。

  • Collection / MutableCollection → java.util.Collection
  • Set / MutableSet → java.util.Set
  • Map / MutableMap → java.util.Map



書いた人: プロダクトデベロップメント部 藤本泰輔