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。
解説
- パッケージに所属すると言われるプロパティ、関数(ここでは
foo関数、プロパティbaz)は実際にはパッケージファサードと呼ばれるKtクラスに入っている。ここではアノテーション@file:JvmName("Comp")が付与されているので、SampleKtクラスではなくCompクラスが生成される。 Quxクラスが生成されるQuxクラスのコンパニオンオブジェクトを表すクラスQux$Companionが生成される。なお、コンパニオンオブジェクトがあるクラスのインナークラスにCompanionクラスを追加することはできない。Quxクラスのコンパニオンオブジェクトのgarply関数で返すインターフェースGarplyの匿名クラスgarply$1。Quxクラスのコンパニオンオブジェクトにてwhenでenumクラスを取った場合に生成されるWhenMappingsクラス。なお、enum以外でwhenによる分岐をした場合にはこのクラスは生成されない。あくまでWhenMappingsクラスはenumの場合のみに生成される。Quuxクラスが生成される。Garplyクラスが生成される。GarplyクラスのコンパニオンオブジェクトGarply$Companionが生成される。Garplyクラスのデフォルト実装を表すGarply$DefaultImplsクラスが生成される。Vipクラスが生成される。Anoクラスが生成される。Ordクラスが生成される。Boolクラスが生成される。- enumのエントリーが実装を持つ場合、それぞれのエントリーごとにクラスが生成されるので、
Bool$OKが生成される。 - 同様に
Bool$NGクラスが生成される。 BoolのコンパニオンオブジェクトBool$Companionが生成される。
では、これらに対してKClassインスタンスを取得して、インスペクションしていく。
KClassの取得
KClassは普通に型名::classで取得すれば良いのだが、一部のクラスはこの方法が使えない。
コンパニオンオブジェクト
コンパニオンオブジェクトを生やしたクラスに続けて.Companionで取得できる。先の例でいくと、Quxクラスのコンパニオンオブジェクトを取得する場合は次のようになる。
val quxCompanionClass = Qux.Companion::class
その他
次のクラスはKotlinコードからクラスを参照できないので、JavaのClass.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 |
qualifiedName、jvmNameにはパッケージ名が付与されるが、ここではデフォルトパッケージで調べているので、パッケージ名がなくなっている。
匿名クラスのqualifiedNameがnullであることがここではポイント。
コンストラクター
コンストラクターを取得できる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を取得してみる。declaredFunctionsはCollection<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が必要になるので、そこだけ注意が必要。