Kotlinのdata classのpropertyをreflectionで更新する

Kotlinのdata classのpropertyをreflectionで更新する方法を調べてみたメモです。


Kotlinのdata classのpropertyをreflectionで更新する必要があったので、方法を調べてみました。
KotlinではJavaのreflection APIとkotlinのreflection API両方使えるので両方で試してみました。

下記バージョンで試してみます。

  • Kotlin 1.3.72

Dependency

gradleを使って試してみます。

kotlin1.3ではreflection APIは標準で含まれてないため、kotlin-reflectをdependencyを追加します。

build.gradle

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

data class

Kotlinでデータを保持するクラスを作成する際、よくdata classを使用してます。
data classはvalでプロパティを宣言して、immutableな構造にすることがほとんどです。

data classにはcopy()という便利なメソッドが生えてるので、
data classのデータを更新する際は copy()で新しいインスタンスを生成します。

data class Person(
    val name: String,
    val age: Int
)

fun main() {
    val person = Person(
        name = "Alice",
        age = 20
    )
    val aged = person.copy(
        age = 21
    )
    println("aged: $aged")
}

結果

aged: Person(name=Alice, age=21)

普通はcopy()を使っておけば困ることはないのですが、
reflectionで更新する必要があったので方法を調べてみました。

varのproperty

下記のようなvarでpropertyが定義されたdata classの場合。

data class MutablePerson(
    var name: String,
    var age: Int
)
KMutableProperty

kotlinのreflectionを使って変更してみます。
KClassのmemberPropertiesから、KMutablePropertyを取得して変更出来ます。

Main.kt

fun main() {
    val mutable = MutablePerson(
        "Anna",
        20
    )
    println("before: $mutable")

    val ageProperty = mutable::class.memberProperties
        .first { it.name == "age" } as KMutableProperty<*>
    ageProperty.isAccessible = true
    ageProperty.setter.call(mutable, 21)

    println("after : $mutable")
}

結果

before: MutablePerson(name=Anna, age=20)
after : MutablePerson(name=Anna, age=21)
Method

Javaのreflectionで更新してみます。
Class.getMethod()でセッターのMethodを取得して更新出来ます。

fun main() {
    val mutable = MutablePerson(
        "Bob",
        30
    )
    println("before: $mutable")

    val setter = mutable::class.java.getMethod("setAge", Int::class.java)
    // val setter = mutable.javaClass.getMethod("setAge", Int::class.java)
    setter.invoke(mutable, 33)

    println("after : $mutable")
}

結果

before: MutablePerson(name=Bob, age=30)
after : MutablePerson(name=Bob, age=33)
Field

もしくはClass.getDeclaredField()でFieldを取得して更新出来ます。

fun main() {
    val mutable = MutablePerson(
        "Cindy",
        40
    )
    println("before: $mutable")

    val age = mutable::class.java.getDeclaredField("age")
    age.isAccessible = true
    age.set(mutable, 44)

    println("after : $mutable")
}

結果

before: MutablePerson(name=Cindy, age=40)
after : MutablePerson(name=Cindy, age=44)

valのproperty

下記のようなvarでpropertyが定義されたdata classの場合。

data class RealOnlyPerson(
    val name: String,
    val age: Int
)
KProperty

Kotlinのreflectionで更新は出来ませんでした。
valで定義されたpropertyの場合、KClass.memberPropertiesで取得出来るのはKProperty1なので、
setterは存在しないので更新出来ません。

fun main() {
    val readOnly = RealOnlyPerson(
        "Anna",
        20
    )
    println("before: $readOnly")

    val ageProperty = readOnly::class.memberProperties
        .first { it.name == "age" } as KProperty<*>
    ageProperty.isAccessible = true
    // Can't call setter method because it's not declared
    // ageProperty.setter.call(readOnly, 21)

    println("after : $readOnly")
}
Method

Javaのreflectionで更新してみます。
mutableなdata classの時のようにClass.getMethod()でセッターのMethodを取得しようとしても取得出来ません。

fun main() {
    // error!!
    /*
    val readOnly = RealOnlyPerson(
        "Bob",
        30
    )
    println("before: $readOnly")

    val setter = readOnly::class.java.getMethod("setAge", Int::class.java)
    setter.invoke(readOnly, 33)

    println("after : $readOnly")
    */
}

結果

Exception in thread "main" java.lang.NoSuchMethodException: com.example.kotlin.reflection.dataclassproperty.RealOnlyPerson.setAge(int)
	at java.base/java.lang.Class.getMethod(Class.java:2108)
	at com.example.kotlin.reflection.dataclassproperty.MainKt.main(Main.kt:97)
	at com.example.kotlin.reflection.dataclassproperty.MainKt.main(Main.kt)

理由は単純で、RealOnlyPersonをDecompileしてみると、
valで定義されたpropertyの場合、getterはありますがsetterは存在しません。
(varで定義すると存在している)

public final class RealOnlyPerson {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public RealOnlyPerson(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   /* 
    〜略〜
   */
}
Field

結果として、valで定義されたpropertyの場合、
Class.getDeclaredField()でFieldを取得して更新出来ます。

fun main() {
    val readOnly = RealOnlyPerson(
        "Cindy",
        40
    )
    println("before: $readOnly")

    val age = readOnly::class.java.getDeclaredField("age")
    age.isAccessible = true
    age.set(readOnly, 44)

    println("after : $readOnly")
}

結果

before: RealOnlyPerson(name=Cindy, age=40)
after : RealOnlyPerson(name=Cindy, age=44)


サンプルコードは下記にあげました
https://github.com/pppurple/kotlin_examples/blob/master/kotlin-reflection-example/src/main/kotlin/com/example/kotlin/reflection/dataclassproperty/Main.kt


【参考】
https://stackoverflow.com/questions/58360868/how-to-change-a-kotlin-private-val-using-reflection
https://stackoverflow.com/questions/35525122/kotlin-data-class-how-to-read-the-value-of-property-if-i-dont-know-its-name-at
https://stackoverflow.com/questions/44304480/how-to-set-delegated-property-value-by-reflection-in-kotlin
https://stackoverflow.com/questions/52512458/how-to-set-val-property-with-kotlin-reflection
https://stackoverflow.com/questions/57090841/set-property-by-string-in-kotlin-using-reflection


おわり。