この記事にまとめた内容を更に発展させたライブラリを公開しました。
解説記事は以下にまとめてあります。
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
有り)
検証には以下のコードを用いました。
検証に用いたプログラムの全体
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.javaConstructor
でConstructor
を取得することができます。
コンストラクタはインスタンスパラメータなどを要求しないため、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.javaMethod
でMethod
を取得することができます。
前述の通り、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
の定義別にJava
のConstructor
/Method
を取得し直接呼び出すパターンをまとめました。
検証の結果、コンストラクタの場合はKFunction
単体からConstructor.newInstance
を呼び出すことができ、それ以外の場合は別口でインスタンスを取得できる場合に限りMethod.invoke
を呼び出せるという結論を得ました。
単純に動くツールを作るだけであればKFunction.callBy
を使うのが最も容易だと思われますが、実行速度にも拘ってツールを作る場合はConstructor
/Method
直接呼び出せるよう工夫してみても良いかもしれません。
この記事が何かのお役に立てば幸いです。