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が必要になるので、そこだけ注意が必要。
KotlinのKClassプロパティを調べた · GitHub