13

Let's say I need to set property A given by a String in Kotlin object O given by a String by reflection. If O was a class I could do something like this (disregard it has no sense):

fun setValue(ownerClassName: String, fieldName: String, value : Any) {
    val enclosingClass = Class.forName(ownerClassName).newInstance()
    val enclosingClassField = enclosingClass.javaClass.getDeclaredField(fieldName)
    enclosingClassField.isAccessible = true
    enclosingClassField.set(enclosingClass, value)
}

But how would I do it if O is an object?

ssuukk
  • 8,200
  • 7
  • 35
  • 47

2 Answers2

25

KClass has an objectInstance field:

Class.forName(ownerClassName).kotlin.objectInstance

This is built into Kotlin reflection.

Returns: The instance of the object declaration, or null if this class is not an object declaration.

This would be even nicer if KClass had a forName method, but sadly it does not (yet), so we need to instead get the (Java) Class and convert it to KClass.

You can get a KClass instance from a Class by using the .kotlin extension property.

Then you can continue with the rest of your code. I converted this to Kotlin's reflection library:

val kClass = Class.forName(ownerClassName).kotlin
// Get the object OR a new instance if it doesn't exist
val instance = kClass.objectInstance ?: kClass.java.newInstance()

val member = kClass.memberProperties
// Has to be a mutable property, otherwise we can't set it
        .filterIsInstance<KMutableProperty<*>>()
// Check the name
        .filter { it.name == fieldName }
        .firstOrNull()

// Set the property
member?.setter?.call(instance, value)

Here is a working test:

object TestObject {
    var field = 3
}

fun setValue(ownerClassName: String, fieldName: String, value: Any) {
    val kClass = Class.forName(ownerClassName).kotlin
    val instance = kClass.objectInstance ?: kClass.java.newInstance()

    val member = kClass.memberProperties.filterIsInstance<KMutableProperty<*>>()
            .firstOrNull { it.name == fieldName }

    member?.setter?.call(instance, value)
}

fun main(args: Array<String>) {
    println(TestObject.field) // 3
    setValue("some.package.TestObject", "field", 4)
    println(TestObject.field) // 4
}
Salem
  • 13,516
  • 4
  • 51
  • 70
  • What do you do once you have this? – Oliver Charlesworth Dec 06 '17 at 13:22
  • It seems OP is wondering what the alternative to `newInstance` would be for an `object` (singleton), since you can't make a new instance. At least that's how I interpreted it. Then you would continue setting the field by using the rest of the code in the question – Salem Dec 06 '17 at 13:23
  • Ok, I think I interpreted the question differently (hence the downvote which I now retracted) - I assumed the OP meant an arbitrary object, rather than a Kotlin singleton object. – Oliver Charlesworth Dec 06 '17 at 13:24
  • Actualy the question was about Kotlin singleton created by "object" keyword as opposed to "class" keyword. – ssuukk Dec 06 '17 at 13:32
  • Well, Aleksiej was first and his answer seems straightforward. And it does what was required. – ssuukk Dec 06 '17 at 13:38
  • @ssuukk the first point may be true, though it is arguably a better idea/more idiomatic to use [`kotlin.reflect`](https://kotlinlang.org/docs/reference/reflection.html). – Salem Dec 06 '17 at 13:40
  • Can you explain why do we need seemingly unused variable "instance" in your answer? – ssuukk Dec 06 '17 at 13:56
  • My bad, I forgot to use the reference while setting, that is fixed now – Salem Dec 06 '17 at 14:10
  • I agree, this is more idiomatic. – Alexey Romanov Dec 06 '17 at 14:34
12

object is translated into a class with a private constructor and a static field called INSTANCE where the only instance is stored when this class is loaded, so replacing Class.forName(ownerClassName).newInstance() with

Class.forName(ownerClassName).getDeclaredField("INSTANCE").get(null)

should work.


Javadoc:

Salem
  • 13,516
  • 4
  • 51
  • 70
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487