LoginSignup
1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-18

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

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


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

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

前置き

本題に入る前に、kotlin.reflect.KFunctionjava.lang.reflect.Constructor/java.lang.reflect.Methodについて書きます。

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

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

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

本文

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

  • コンストラクタ
  • コンストラクタ以外
    • インスタンス
    • コンパニオンオブジェクト
    • コンパニオンオブジェクト(@JvmStatic有り)

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

検証に用いたプログラムの全体
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"))
        }
    }
}

コンストラクタの場合

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

@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.javaMethodMethodを取得することができます。
前述の通り、Methodを呼び出すには、インスタンスパラメータ(static methodの場合null、それ以外はインスタンス)が必要になります。

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

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

インスタンス関数の場合

@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"))
    }
}

コンパニオンオブジェクトに定義した関数の場合

@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有り)の場合2

@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の定義別にJavaConstructor/Methodを取得し直接呼び出すパターンをまとめました。
検証の結果、コンストラクタの場合はKFunction単体からConstructor.newInstanceを呼び出すことができ、それ以外の場合は別口でインスタンスを取得できる場合に限りMethod.invokeを呼び出せるという結論を得ました。

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

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


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

  2. こちらの記事にまとめた通り、Kotlin上で定義した関数はstaticFunctionsで取得できないため、この例では省略しています。 

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0