【Kotlin】KFunctionからJavaのConstructor/Methodを取得し呼び出す

この記事にまとめた内容を更に発展させたライブラリを公開しました。

- [ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction\.](https://github.com/ProjectMapK/FastKFunction)

解説記事は以下にまとめてあります。

- [【Kotlin】KFunctionを高速に呼び出す（後編） \- Qiita](https://qiita.com/wrongwrong/items/fe75bae3911eff319e68)
- [【自作ライブラリ】FastKFunctionはどれ位速いのか【Kotlin】 \- Qiita](https://qiita.com/wrongwrong/items/39be6adf8cc38b85d6ed)

---

`Kotlin`のリフレクションでのメソッド/コンストラクタである`KFunction`は様々な部分が抽象化されており、アプリケーションからの利用が容易となっています。
一方、`Java`の`Constructor`/`Method`を直接呼び出した場合と比べ、`KFunction`は抽象化によるオーバーヘッドから呼び出しが遅いという問題が有ります。

この記事では、このオーバーヘッドを回避するため、`KFunction`の定義別に`Java`の`Constructor`/`Method`を取得し直接呼び出すパターンをまとめます。
バージョンはそれぞれ`Kotlin 1.4.10`/`Java8`です。

# 前置き
本題に入る前に、`kotlin.reflect.KFunction`と`java.lang.reflect.Constructor`/`java.lang.reflect.Method`について書きます。

パッケージ名の通り、前者は`Kotlin`のリフレクションでの関数です。
`Kotlin`では、メソッドとコンストラクタは同じ`KFunction`として同様に扱えます。
`KFunction`は、`callBy`呼び出しによってデフォルト引数を用いたり、特定条件で`Java`の`Method.invoke`におけるインスタンス引数を無視することができます。

後者は`Java`のリフレクションでの関数です。
`Constructor`と`Method`は型として分かれているため同様には取り扱えず、また`Method`はインスタンスパラメータ（`static method`の場合`null`、それ以外はインスタンス）を要求します。

冒頭で書いた「`Java`の`Constructor`/`Method`を直接呼び出した場合と比べ、`KFunction`は呼び出しが遅い」というのは、デフォルト引数を用いず（= 全パラメータを揃えて）`KFunction.call`を呼び出した場合と`Constructor.newInstance`/`Method.invoke`を呼び出した場合との比較です。

# 本文
関数の定義方法は大まかに2種類、細かく見て4種類としてそれぞれについてまとめます。

- コンストラクタ
- コンストラクタ以外
  - インスタンス
  - コンパニオンオブジェクト
  - コンパニオンオブジェクト（`@JvmStatic`有り）

検証には以下のコードを用いました。

<details><summary>検証に用いたプログラムの全体</summary><div>

```kotlin
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import kotlin.reflect.KFunction
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.functions
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaMethod

@Suppress("UNCHECKED_CAST")
class CallJavaReflectionTest {
    data class ConstructorSample(val foo: Int, val bar: String)

    fun instanceMethodSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)

    companion object {
        fun companionObjectFunctionSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)

        @JvmStatic
        fun staticMethodSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)
    }

    val expected = ConstructorSample(1, "2")

    @Test
    @DisplayName("コンストラクタの場合")
    fun constructorTest() {
        val function: KFunction<ConstructorSample> = ::ConstructorSample
        assertEquals(expected, function.call(1, "2"))

        val javaConstructor: Constructor<ConstructorSample> = function.javaConstructor!!
        assertEquals(expected, javaConstructor.newInstance(1, "2"))
    }

    @Test
    @DisplayName("インスタンス関数の場合")
    fun instanceMethodTest() {
        val function: KFunction<ConstructorSample> = this::instanceMethodSample
        assertEquals(expected, function.call(1, "2"))

        val javaMethod: Method = function.javaMethod!!
        assertEquals(expected, javaMethod.invoke(this, 1, "2"))
    }

    @Nested
    @DisplayName("コンパニオンオブジェクトに定義したメソッドの場合")
    inner class CompanionObjectFunctionTest {
        @Test
        @DisplayName("メソッドリファレンスで取得した場合")
        fun byMethodReferenceTest() {
            val function: KFunction<ConstructorSample> = (CallJavaReflectionTest)::companionObjectFunctionSample
            // メソッドリファレンスで取得した場合インスタンスパラメータが不要
            assertEquals(expected, function.call(1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }

        @Test
        @DisplayName("リフレクションで取得した場合")
        fun byReflectionTest() {
            val function: KFunction<ConstructorSample> = CallJavaReflectionTest::class.companionObject!!
                .functions.first { it.name == "companionObjectFunctionSample" }
                .let { it as KFunction<ConstructorSample> }
            // リフレクションで取得した場合インスタンスパラメータが必要
            assertEquals(expected, function.call(CallJavaReflectionTest::class.companionObjectInstance, 1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }
    }

    @Nested
    @DisplayName("コンパニオンオブジェクトに定義したメソッド（JvmStatic指定有り）の場合")
    inner class StaticMethodTest {
        @Test
        @DisplayName("メソッドリファレンスで取得した場合")
        fun byMethodReferenceTest() {
            val function: KFunction<ConstructorSample> = (CallJavaReflectionTest)::staticMethodSample
            assertEquals(expected, function.call(1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }

        @Test
        @DisplayName("コンパニオンオブジェクトからリフレクションで取得した場合")
        fun byReflectionTest() {
            val function: KFunction<ConstructorSample> = CallJavaReflectionTest::class.companionObject!!
                .functions.first { it.name == "staticMethodSample" }
                .let { it as KFunction<ConstructorSample> }
            // リフレクションで取得した場合インスタンスパラメータが必要
            assertEquals(expected, function.call(CallJavaReflectionTest::class.companionObjectInstance, 1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }
    }
}

```

</div></details>

## コンストラクタの場合
`KFunction`の生成元がコンストラクタの場合、`KFunction.javaConstructor`で`Constructor`を取得することができます。
コンストラクタはインスタンスパラメータなどを要求しないため、`KFunction.call`と同じように`Constructor.newInstance`を呼び出すことができます。

```kotlin
@Suppress("UNCHECKED_CAST")
class CallJavaReflectionTest {
    data class ConstructorSample(val foo: Int, val bar: String)

    val expected = ConstructorSample(1, "2")

    @Test
    @DisplayName("コンストラクタの場合")
    fun constructorTest() {
        val function: KFunction<ConstructorSample> = ::ConstructorSample
        assertEquals(expected, function.call(1, "2"))

        val javaConstructor: Constructor<ConstructorSample> = function.javaConstructor!!
        assertEquals(expected, javaConstructor.newInstance(1, "2"))
    }
}
```

## コンストラクタ以外の場合
`KFunction`の生成元がコンストラクタ以外の場合、`KFunction.javaMethod`で`Method`を取得することができます。
前述の通り、`Method`を呼び出すには、インスタンスパラメータ（`static method`の場合`null`、それ以外はインスタンス）が必要になります。

ここで、コンストラクタ以外の場合、生成方法に関わらず`KFunction`からインスタンスを取得する方法が公開されていないため、`KFunction.call`でインスタンスパラメータが省略されている場合でも、`KFunction`単体から`Method.invoke`を呼び出すことはできません[^instance_hosoku]。
`JvmStatic`を付けた場合でも、`KFunction`として取得する方法で取得できるのは`companion object`への定義となるため、インスタンスパラメータを`null`として`Method.invoke`を呼び出すことはできません。

[^instance_hosoku]: `KFunction.call`でインスタンスパラメータが省略できる以上、内部的には何らかの方法でインスタンスの情報を保持していると考えられますが、`KFunction`のインターフェースではそれが公開されておらず、`KFunction`の実装クラスである`KFunctionImpl`は`internal`クラスであるため、これ以上追いかけると本当の黒魔術になると判断し、ここでは「できない」を結論とします。

従って、コンストラクタ以外から取得した`KFunction`から`Method`を呼び出すには、別口でインスタンスを用意する必要が有ります。

### インスタンス関数の場合
```kotlin
@Suppress("UNCHECKED_CAST")
class CallJavaReflectionTest {
    data class ConstructorSample(val foo: Int, val bar: String)

    fun instanceMethodSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)

    val expected = ConstructorSample(1, "2")

    @Test
    @DisplayName("インスタンス関数の場合")
    fun instanceMethodTest() {
        val function: KFunction<ConstructorSample> = this::instanceMethodSample
        assertEquals(expected, function.call(1, "2"))

        val javaMethod: Method = function.javaMethod!!
        assertEquals(expected, javaMethod.invoke(this, 1, "2"))
    }
}
```

### コンパニオンオブジェクトに定義した関数の場合
```kotlin
@Suppress("UNCHECKED_CAST")
class CallJavaReflectionTest {
    data class ConstructorSample(val foo: Int, val bar: String)

    companion object {
        fun companionObjectFunctionSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)
    }

    val expected = ConstructorSample(1, "2")

    @Nested
    @DisplayName("コンパニオンオブジェクトに定義したメソッドの場合")
    inner class CompanionObjectFunctionTest {
        @Test
        @DisplayName("メソッドリファレンスで取得した場合")
        fun byMethodReferenceTest() {
            val function: KFunction<ConstructorSample> = (CallJavaReflectionTest)::companionObjectFunctionSample
            // メソッドリファレンスで取得した場合インスタンスパラメータが不要
            assertEquals(expected, function.call(1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }

        @Test
        @DisplayName("リフレクションで取得した場合")
        fun byReflectionTest() {
            val function: KFunction<ConstructorSample> = CallJavaReflectionTest::class.companionObject!!
                .functions.first { it.name == "companionObjectFunctionSample" }
                .let { it as KFunction<ConstructorSample> }
            // リフレクションで取得した場合インスタンスパラメータが必要
            assertEquals(expected, function.call(CallJavaReflectionTest::class.companionObjectInstance, 1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }
    }
}
```

### コンパニオンオブジェクト（`@JvmStatic`有り）の場合[^static_hosoku]
[^static_hosoku]: [こちらの記事](https://qiita.com/wrongwrong/items/1fa0aece9d32bf5e86aa)にまとめた通り、`Kotlin`上で定義した関数は`staticFunctions`で取得できないため、この例では省略しています。

```kotlin
@Suppress("UNCHECKED_CAST")
class CallJavaReflectionTest {
    data class ConstructorSample(val foo: Int, val bar: String)

    companion object {
        @JvmStatic
        fun staticMethodSample(foo: Int, bar: String): ConstructorSample = ConstructorSample(foo, bar)
    }

    val expected = ConstructorSample(1, "2")

    @Nested
    @DisplayName("コンパニオンオブジェクトに定義したメソッド（JvmStatic指定有り）の場合")
    inner class StaticMethodTest {
        @Test
        @DisplayName("メソッドリファレンスで取得した場合")
        fun byMethodReferenceTest() {
            val function: KFunction<ConstructorSample> = (CallJavaReflectionTest)::staticMethodSample
            assertEquals(expected, function.call(1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }

        @Test
        @DisplayName("コンパニオンオブジェクトからリフレクションで取得した場合")
        fun byReflectionTest() {
            val function: KFunction<ConstructorSample> = CallJavaReflectionTest::class.companionObject!!
                .functions.first { it.name == "staticMethodSample" }
                .let { it as KFunction<ConstructorSample> }
            // リフレクションで取得した場合インスタンスパラメータが必要
            assertEquals(expected, function.call(CallJavaReflectionTest::class.companionObjectInstance, 1, "2"))

            val javaMethod: Method = function.javaMethod!!
            assertEquals(expected, javaMethod.invoke(CallJavaReflectionTest::class.companionObjectInstance!!, 1, "2"))
        }
    }
}
```

# まとめ
この記事では`KFunction`の呼び出しオーバーヘッドを回避するため、`KFunction`の定義別に`Java`の`Constructor`/`Method`を取得し直接呼び出すパターンをまとめました。
検証の結果、コンストラクタの場合は`KFunction`単体から`Constructor.newInstance`を呼び出すことができ、それ以外の場合は別口でインスタンスを取得できる場合に限り`Method.invoke`を呼び出せるという結論を得ました。

単純に動くツールを作るだけであれば`KFunction.callBy`を使うのが最も容易だと思われますが、実行速度にも拘ってツールを作る場合は`Constructor`/`Method`直接呼び出せるよう工夫してみても良いかもしれません。

この記事が何かのお役に立てば幸いです。