mike-neckのブログ

Java or Groovy or Swift or Golang

Kotlinのリフレクション(KClass)を調べた

KuickCheckを作った時にKotlinのリフレクションを一通り触ったが、完全に忘れてしまったのであらためてメモ。

ここではクラスを表すKClassとそのプロパティ、およびそれらが適用できるクラスをまとめてある。


Kotlinコンパイラーが生成するクラス

KClassのプロパティなどについて確認する前に、Kotlinコンパイラーが生成するクラスについてまとめておきます。

クイズ

次のKotlinファイルSample.ktコンパイルして生成されるクラスの数はいくつか。

@file:JvmName("Comp")

import java.io.Closeable

fun foo(bar: String): String = bar.toUpperCase()
val baz: Int = foo("baz").length

class Qux {
    val prop: String = "ooo"
    fun function(t: Int, msg: String): List<String> =
            if (t < 0) emptyList() else (1..t).map { "$msg$it" }
    fun <R: Closeable> R.tryAndClose(action: (R) -> Unit) {
        try {
            action(this)
        } finally {
            this.close()
        }
    }
    val Quux.id: String
        get() = this.name
    fun Qux.foop(): String = this.prop.plus(this.toString())

    companion object {
        @JvmStatic fun add(q: Qux, qx: Quux): Int =
                q.prop.length + qx.name.length
        fun garply(q: Quux): Garply = object: Garply {
            override val waldo: String = q.name
        }
        fun boolean(b: Bool): String = when (b) {
            Bool.OK -> "true"
            Bool.NG -> "false"
        }
    }
}

class Quux(val name: String) {
    constructor(q: Qux): this(q.prop)
}

interface Garply {
    val waldo: String
    fun lorem(q: Qux): Quux = Quux(q.prop)
    val dp: Int
        get() = 1
    companion object: Garply {
        override val waldo: String = "waldo"
    }
}

object Vip

annotation class Ano(val name: String)

enum class Ord {
    LT,EQ,GT
}
enum class Bool(val asBoolean: Boolean) {
    OK(true ){ override val int: Int = 1 },
    NG(false){ override val int: Int = 0 };
    abstract val int: Int

    fun Vip.size(): Int = 10
    val Ano.size: Int
        get() = this.name.length
    companion object {
        fun list(): List<Bool> = values().toList()
    }
}

正解

クラスは16。

解説

  1. パッケージに所属すると言われるプロパティ、関数(ここではfoo関数、プロパティbaz)は実際にはパッケージファサードと呼ばれるKtクラスに入っている。ここではアノテーション@file:JvmName("Comp")が付与されているので、SampleKtクラスではなくCompクラスが生成される。
  2. Quxクラスが生成される
  3. Quxクラスのコンパニオンオブジェクトを表すクラスQux$Companionが生成される。なお、コンパニオンオブジェクトがあるクラスのインナークラスにCompanionクラスを追加することはできない。
  4. Quxクラスのコンパニオンオブジェクトのgarply関数で返すインターフェースGarplyの匿名クラスgarply$1
  5. Quxクラスのコンパニオンオブジェクトにてwhenenumクラスを取った場合に生成されるWhenMappingsクラス。なお、enum以外でwhenによる分岐をした場合にはこのクラスは生成されない。あくまでWhenMappingsクラスはenumの場合のみに生成される。
  6. Quuxクラスが生成される。
  7. Garplyクラスが生成される。
  8. GarplyクラスのコンパニオンオブジェクトGarply$Companionが生成される。
  9. Garplyクラスのデフォルト実装を表すGarply$DefaultImplsクラスが生成される。
  10. Vipクラスが生成される。
  11. Anoクラスが生成される。
  12. Ordクラスが生成される。
  13. Boolクラスが生成される。
  14. enumのエントリーが実装を持つ場合、それぞれのエントリーごとにクラスが生成されるので、Bool$OKが生成される。
  15. 同様にBool$NGクラスが生成される。
  16. BoolのコンパニオンオブジェクトBool$Companionが生成される。

では、これらに対してKClassインスタンスを取得して、インスペクションしていく。

KClassの取得

KClassは普通に型名::classで取得すれば良いのだが、一部のクラスはこの方法が使えない。

コンパニオンオブジェクト

コンパニオンオブジェクトを生やしたクラスに続けて.Companionで取得できる。先の例でいくと、Quxクラスのコンパニオンオブジェクトを取得する場合は次のようになる。

val quxCompanionClass = Qux.Companion::class

その他

次のクラスはKotlinコードからクラスを参照できないので、JavaClass.forName(String)から取得した後、Classインスタンスに生えているkotlinプロパティでKClassインスタンスを取得する。

  • Ktクラス
  • 匿名クラス
  • WhenMappings
  • DefaultImpls
  • enumエントリー

先程の例ではGarplyクラスのデフォルト実装クラスGarply$DefaultImplsクラスは次のように取得する。

val anonymusClass = Class.forName("Garply${'$'}DefaultImpls").kotlin

なお、kotlin extensionプロパティはnullable型(KClass<T>?)ではないので、KClass型で取得できる。

name系のプロパティ

まずはname系のプロパティであるこれらのプロパティ。

  • qualifiedName - 完全修飾名
  • simpleName - クラス名のみ
  • jvmName - JVM上でのクラス名
クラス qualifiedName simpleName jvmName
Ktクラス Comp Comp Comp
通常のクラス Qux Qux Qux
コンパニオン Qux.Companion Companion Qux$Companion
匿名クラス null 1 Qux$Companion$garply$1
WhenMappings Qux$Companion$WhenMappings Qux$Companion$WhenMappings Qux$Companion$WhenMappings
インターフェース Garply Garply Garply
DefaultImpls Garply.DefaultImpls DefaultImpls Garply.DefaultImpls
オブジェクトクラス Vip Vip Vip
アノテーション Ano Ano Ano
enum(実装なし) Ord Ord Ord
enum(実装あり) Bool Bool Bool
enumエントリー Bool.OK OK Bool$OK

qualifiedNamejvmNameにはパッケージ名が付与されるが、ここではデフォルトパッケージで調べているので、パッケージ名がなくなっている。

匿名クラスのqualifiedNamenullであることがここではポイント。

コンストラクター

コンストラクターを取得できるKClassのプロパティは次の二つ。

  • primaryConstructor - プライマリコンストラクタ(KFunction<T>)を返す
  • constructors - コンストラクターすべてを返す(Collection<KFunction<T>>)

KFunctionにはnameというプロパティがあるので、それを取得してみた。

クラス primaryConstructor constructors
Ktクラス 取得できない(1) 取得できない(1)
通常のクラス <init> (<init>)
コンパニオン null ()
匿名クラス <init> (<init>)
WhenMappings 取得できない(1) 取得できない(1)
インターフェース null ()
DefaultImpls 取得できない(1) 取得できない(1)
オブジェクトクラス null ()
アノテーション null ()
enum(実装なし) <init> (<init>)
enum(実装あり) <init> (<init>)
enumエントリー null ()

(1) -- プロパティにアクセスすると例外(UnsupportedOperationException)が発生する

Ktクラス、WhenMappings、DefaultImplsはこの後で調べるプロパティも例外を返してくるほど、リフレクションでの操作がサポートされていない状況。関数なども取得できないということは、これらのクラスに入っている関数をKotlinの表現で利用することもできない。したがって、これらのクラスに入っている関数を利用したい場合はJavaのリフレクションを利用することになる。

通常のクラスにconstructorを追加すれば、もちろんconstructorsで返ってくるコンストラクターの数も増える。

インナークラス

nestedClassで取得できるクラス(KClass)。ここでは取得できたインナークラスのsimpleNameを取得した。

先の例では明示的に作成していないが…

クラス nestedClass
通常のクラス (Companion)
コンパニオン ()
匿名クラス ()
インターフェース (Companion)
オブジェクトクラス ()
アノテーション ()
enum(実装なし) ()
enum(実装あり) (Companion)
enumエントリー ()

(*) Ktクラス、WhenMappings、DefaultImplsは例外を出すので除外した

コンパニオンオブジェクトがインナークラスとして取得できる。まあ、インナークラスといえばインナークラスといえる。また、WhenMappings、DefaultImplsなどは取得できないことから、これらがインナークラスとみなされていないことがわかる。

関数

関数を取得するプロパティがいくつかあるが、まずはdeclaredFunctionsを取得してみる。declaredFunctionsCollection<KFunction<T>>を返すので、nameプロパティを取得した。

クラス declaredFunctions
通常のクラス (function,foop,tryAndClose)
コンパニオン (add,boolean,garply)
匿名クラス ()
インターフェース (lorem)
オブジェクトクラス ()
アノテーション ()
enum(実装なし) 取得できない(2)
enum(実装あり) 取得できない(2)
enumエントリー ()

(2) KotlinReflectionInternalErrorが発生する。この例外はenumクラスの関数、プロパティなどを取得しようとするとちょくちょく発生する。

declaredFunctionsはクラスの中で定義した関数(いわゆるメソッド)および、クラスの中で定義した拡張関数を返す。

また、コンパニオンオブジェクトで@JvmStaticを付与した関数も返ってくる。まあ、JavaでスタティックメソッドがClass#getMethod(String, Class...)で返ってくることを考えれば当然といえば当然なのだが…


次にdeclaredMemberFunctionsを取得する。

クラス declaredMemberFunctions
通常のクラス (function)
コンパニオン (add,boolean,garply)
匿名クラス ()
インターフェース (lorem)
オブジェクトクラス ()
アノテーション ()
enum(実装なし) ()
enum(実装あり) ()
enumエントリー ()

declaredMemberFunctionの場合はクラスの中で定義している拡張関数以外の関数が返ってくることが、Quxクラスの結果を見るとわかる。


次にdeclaredMemberExtensionFunctionsを取得してみる。

クラス declaredMemberExtensionFunctions
通常のクラス (foop,tryAndClose)
コンパニオン ()
匿名クラス ()
インターフェース ()
オブジェクトクラス ()
アノテーション ()
enum(実装なし) ()
enum(実装あり) (size)
enumエントリー ()

declaredMemberFunctionsとは異なり、クラスの中で定義している拡張関数が返ってくることが、Quxクラスの取得結果からわかる。

プロパティ

プロパティも同様に様々あるが、まずmemberPropertiesを取得してみる。この関数はColleciton<KProperty1<T,*>>を返してくる。KProperty1<T,R>KProperty<R>KCallable<R>を継承しており、nameが取得できるので、ここではnameを取得した。

クラス memberProperties
通常のクラス (prop)
コンパニオン ()
匿名クラス (waldo,dp)
インターフェース (dp,waldo)
オブジェクトクラス ()
アノテーション (name)
enum(実装なし) 取得できない
enum(実装あり) 取得できない
enumエントリー 取得できない

クラスにて取得できるプロパティが返されることが、Quxおよび匿名クラスの結果からわかる。

そして相変わらずenumで例外が発生する。


次にdeclaredMemberPropertiesを取得してみる。

クラス declaredMemberProperties
通常のクラス (prop)
コンパニオン ()
匿名クラス (waldo)
インターフェース (dp,waldo)
オブジェクトクラス ()
アノテーション (name)
enum(実装なし) ()
enum(実装あり) (asBoolean,int)
enumエントリー ()

ボディで定義したプロパティが返されることが、Quxおよび匿名クラスとGarplyインターフェースの取得結果からわかる。

declaredMemberPropertiesを使うと、enumクラスは例外が発生しない…


次にdeclaredMemberExtensionPropertiesを取得してみる。

クラス declaredMemberExtensionProperties
通常のクラス (id)
コンパニオン ()
匿名クラス ()
インターフェース ()
オブジェクトクラス ()
アノテーション ()
enum(実装なし) ()
enum(実装あり) (size)
enumエントリー ()

こちらは、ボディで定義した拡張プロパティが取得できるようである。

スタティックなんたら

staticFunctionsというのがあるので取得してみる。型はCollection<KFunction<T>>

クラス staticFunctions
通常のクラス ()
コンパニオン ()
匿名クラス ()
インターフェース ()
オブジェクトクラス ()
アノテーション ()
enum(実装なし) 取得できない
enum(実装あり) 取得できない
enumエントリー ()

@JvmStaticを付与した関数が取得できるのかと思いきや、何も取得できない。

まとめない

取り上げてないプロパティがいくつかあるが、とりあえず、ここまでわかれば大体なんとかなると思う。KotlinのリフレクションはおおよそJavaのリフレクションをKotlinの言語モデルに変換するためのラッパーという感じ。実際にKCallableの引数の型を見ても、第一引数がその関数が所属しているクラスのインスタンスだったりする。

各プロパティを取得するためのコードはgistに貼っておくので、興味ある人はやってみるといいと思う。なお、実効にはkotlin-reflection.jarが必要になるので、そこだけ注意が必要。

KotlinのKClassプロパティを調べた · GitHub