この記事にまとめた内容を更に発展させたライブラリを公開しました。
解説記事は以下にまとめてあります。
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直接呼び出せるよう工夫してみても良いかもしれません。
この記事が何かのお役に立てば幸いです。