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

KotlinでJSR303(Bean Validation)を使うときの注意

少しハマったのでメモ。

KotlinでBean Validationを使った時に、validationされないので、Stackoverflowなどを読んでいたら、アノテーションの書き方を工夫する旨あった。

まずはvalidationされないパターン

data class Person (
        @NotEmpty
        val name: String,
        @Min(18)
        val age: Int)

この場合、アノテーションコンストラクターに付与されたとみなされてしまうのでvalidationされないとのこと。

validationされるパターン

data class Person (
        @get:NotEmpty
        val name: String,
        @get:Min(18)
        val age: Int)

get:で始めると、getterの方にアノテーションが付与されるので、Bean validationに通すことができる。

Kotlin用のjunit-quickcheck風なテスティングフレームワークを作った

表記のとおりです。

テストデータをランダムに自動生成して複数回実行させるというライブラリーです。調べてみたところ、Kotlin用のものがないようなので、作ってみました。名前はKuickCheckという名前にしていますが、今後どうなるかわかりません。あと、僕は残念ながらProperty based testingに詳しくありませんし、QuickCheckとかScalaCheckとかScalaPropのこともよく知りません(不勉強)。

github.com

入手方法

Mavenにあがっています。

<dependency>
    <groupId>org.mikeneck</groupId>
    <artifactId>kuickcheck-core</artifactId>
    <version>0.1</version>
    <scope>test</scope>
</dependency>
  • Gradle
dependency {
  testCompile 'org.mikeneck:kuickcheck-core:0.1'
}

依存ライブラリーはkotlin-runtimeとkotlin-reflectだけです。

JUnitにまったく依存していないので、Junitとともに走らせることはできません。

テストの書き方

テストの書き方はシンプルです。

  1. クラスかオブジェクトを作成します。
  2. forAllメソッドにチェックしたいデータを指定します(下のサンプルでは正のintであるpositiveIntを指定しています)。
  3. チェックする性質をsatisfy{}ブロックに記述します。
  4. 1-3によって返されるオブジェクトをなんらかのメンバーに指定します。このメンバーにはテストする性質がわかるような名前をつけます。
  5. 4.に対して@Propertyアノテーションを付与します。

これにより、ランダムな値でデフォルトで100回のテストを実行します。

object GettingStarted {

  @Property
  val positiveNumberIsMoreThan0 =
      forAll(positiveInt).satisfy{ it > 0 }

  @Property
  val `positiveInt x negativeInt becomes negative` =
      forAll(positiveInt, negativeInt).satisfy {l,r -> l * r < 0}
}

テストの実行

テストの実行は今のところjava -jarによる実行方法しかありません。頑張って作成したテストのjar、kotlin-runtime.jar、kotlin-reflect.jar、kuickcheck.jarを集めて、org.mikeneck.kuickcheck.KuickCheckをメインに指定して実行します。Gradleを使っている場合はJavaExecタスクで実行するのがよいでしょう。

f:id:mike_neck:20160613164251p:plain

実行すると、このような形で成功したテストが緑、失敗したデータが有る場合は赤、例外が発生した場合は黄で表示されます。

Javaとのinterop

試していないのでわかりません(´・ω・`)

intとかlongとかJavaのプリミティブ型と同じ名前のAPIを準備しているのでコンパイルできないのではないかと思います。

この後

一応、今のところできているのは

  • パッケージをスキャンしてテストを抽出
  • intlongBigIntegerなどの基本的な型の値を自動で生成してテストに流す
  • テスト結果をコンソールに表示

です。

できていないところは

  • テスト結果をファイルに吐き出す
  • Ktで終わるソースを検知できない(Ktファイルを除外するために末尾がKtのものを除外している)
  • JUnitに依存していないので、JUnitとともに走らせることができない

などなどです。

Kotlinのコンパイラーが吐き出すクラスまとめ

リフレクションをいろいろいじっていて気になったので中途半端にまとめてみた

Kotlin クラスのメモ

Kotlinをコンパイルすると生成されるクラス

  • クラス/インターフェース/enum/オブジェクト1つにつき1つのクラス
  • パッケージに所属する関数/プロパティ -> Ktクラス
  • インターフェースに記述した実装 -> $DefaultImplsクラス
  • 関数クラスやobjectワードによって生成される実装クラス -> $1など数字で終わるクラス
  • enumのメンバーに実装する -> EnumClass$Memberクラス
  • when式でenumによる条件マッピング -> $WhenMappingsクラス
  • コンパニオンオブジェクト -> $Companionクラス

例: compile-sample.kt

fun hello(name: String) = "Hello, $name"
class Foo {
  val upper: (String) -> String = String::toUpperCase
  companion object {
    fun printable(c: Color): Boolean =
        when(c) {
          BLACK -> true
          WHITE -> false
          BLUE  -> true
        }
  }
}
enum class Color {
  BLACK { override fun asString() = "black" },
  WHITE { override fun asString() = "white" },
  BLUE  { override fun asString() = "blue"  };
  abstract fun asString(): String
}
interface Bar {
  val name: String
  fun plus(x: Int, y: Int): Int = x + y
}

上記のコードをコンパイルした場合は次のクラスファイルが生成される

  • Bar$DefaultImpls.class
  • Bar.class
  • Color$BLACK.class
  • Color$BLUE.class
  • Color$WHITE.class
  • Color.class
  • Compile_sampleKt.class
  • Foo$Companion$WhenMappings.class
  • Foo$Companion.class
  • Foo$upper$1.class
  • Foo.class

なお、これらのファイルをjavapすると次のようになる。

Bar$DefaultImpls.class

Compiled from "compile-sample.kt"
public final class Bar$DefaultImpls {
  public static int plus(Bar, int, int);
}

Bar.class

Compiled from "compile-sample.kt"
public interface Bar {
  public abstract java.lang.String getName();
  public abstract int plus(int, int);
}

Color$BLACK.class

Compiled from "compile-sample.kt"
public final class Color$BLACK extends Color {
  public java.lang.String asString();
  Color$BLACK();
}

Color.class

Compiled from "compile-sample.kt"
public abstract class Color extends java.lang.Enum<Color> {
  public static final Color BLACK;
  public static final Color WHITE;
  public static final Color BLUE;
  static {};
  public abstract java.lang.String asString();
  protected Color();
  public static Color[] values();
  public static Color valueOf(java.lang.String);
}

Compile_sampleKt.class

Compiled from "compile-sample.kt"
public final class Compile_sampleKt {
  public static final java.lang.String hello(java.lang.String);
}

Foo$Companion$WhenMappings.class

Compiled from "compile-sample.kt"
public final class Foo$Companion$WhenMappings {
  public static final int[] $EnumSwitchMapping$0;
  static {};
}

Foo$Companion.class

Compiled from "compile-sample.kt"
public final class Foo$Companion {
  public final boolean printable(Color);
  public Foo$Companion(kotlin.jvm.internal.DefaultConstructorMarker);
}

Foo$upper$1.class

Compiled from "compile-sample.kt"
final class Foo$upper$1 extends kotlin.jvm.internal.FunctionReference implements kotlin.jvm.functions.Function1<java.lang.String, java.lang.String> {
  public static final Foo$upper$1 INSTANCE;
  public java.lang.Object invoke(java.lang.Object);
  public final java.lang.String invoke(java.lang.String);
  public final kotlin.reflect.KDeclarationContainer getOwner();
  public final java.lang.String getName();
  public final java.lang.String getSignature();
  Foo$upper$1();
  static {};
}

Foo.class

Compiled from "compile-sample.kt"
public final class Foo {
  public static final Foo$Companion Companion;
  public final kotlin.jvm.functions.Function1<java.lang.String, java.lang.String> getUpper();
  public Foo();
  static {};
}

特にオチはない(おわり)