😴 🧙🌈 ʕ•ᴥ•ʔ

The following article is a complete rundown on the available API to use and abuse of JVM reflection using Kotlin.

https://kt.academy/article/ak-reflection


Reflection in programming is a program’s ability to introspect its own source code symbols at runtime. For example, it might be used to display all of a class’s properties, like in the displayPropertiesAsList function below.

import kotlin.reflect.full.memberProperties

fun displayPropertiesAsList(value: Any) {
    value::class.memberProperties
        .sortedBy { it.name }
        .map { p -> " * ${p.name}: ${p.call(value)}" }
        .forEach(::println)
}

class Person(
    val name: String,
    val surname: String,
    val children: Int,
    val female: Boolean,
)

class Dog(
    val name: String,
    val age: Int,
)

enum class DogBreed {
    HUSKY, LABRADOR, PUG, BORDER_COLLIE
}

fun main() {
    val granny = Person("Esmeralda", "Weatherwax", 0, true)
    displayPropertiesAsList(granny)
    // * children: 0
    // * female: true
    // * name: Esmeralda
    // * surname: Weatherwax

    val cookie = Dog("Cookie", 1)
    displayPropertiesAsList(cookie)
    // * age: 1
    // * name: Cookie

    displayPropertiesAsList(DogBreed.BORDER_COLLIE)
    // * name: BORDER_COLLIE
    // * ordinal: 3
}

Reflection is often used by libraries that analyze our code and behave according to how it is constructed. Let’s see a few examples.

Libraries like Gson use reflection to serialize and deserialize objects. These libraries often reference classes to check which properties they require and which constructors they offer in order to then use these constructors. Later in this chapter, we will implement our own serializer.

data class Car(val brand: String, val doors: Int)

fun main() {
    val json = "{\"brand\":\"Jeep\", \"doors\": 3}"
    val gson = Gson()
    val car: Car = gson.fromJson(json, Car::class.java)
    println(car) // Car(brand=Jeep, doors=3)
    val newJson = gson.toJson(car)
    println(newJson) // {"brand":"Jeep", "doors": 3}
}

As another example, we can see the Koin dependency injection framework, which uses reflection to identify the type that should be injected and to create and inject an instance appropriately.

class MyActivity : Application() {
    val myPresenter: MyPresenter by inject()
}

Reflection is extremely useful, so let’s start learning how we can use it ourselves.

To use Kotlin reflection, we need to add the kotlin-reflect dependency to our project. It is not needed to reference an element, but we need it for the majority of operations on element references. If you see KotlinReflectionNotSupportedError, it means the kotlin-reflect dependency is required. In the code examples in this chapter, I assume this dependency is included.

Hierarchy of classes

Before we get into the details, let’s first review the general type hierarchy of element references.

Notice that all the types in this hierarchy start with the K prefix. This indicates that this type is part of Kotlin Reflection and differentiates these classes from Java Reflection. The type Class is part of Java Reflection, so Kotlin called its equivalent KClass.

At the top of this hierarchy, you can find KAnnotatedElement. Element is a term that includes classes, functions, and properties, so it includes everything we can reference. All elements can be annotated, which is why this interface includes the annotations property, which we can use to get element annotations.

interface KAnnotatedElement {
    val annotations: List<Annotation>
}

The next confusing thing you might have noticed is that there is no type to represent interfaces. This is because interfaces in reflection API nomenclature are also considered classes, so their references are of type KClass. This might be confusing, but it is really convenient.

Now we can get into the details, which is not easy because everything is connected to nearly everything else. At the same time, using the reflection API is really intuitive and easy to learn. Nevertheless, I decided that to help you understand this API better I’ll do something I generally avoid doing: we will go through the essential classes and discuss their methods and properties. In between, I will show you some practical examples and explain some essential reflection concepts.

Function references

We reference functions using a double colon and a function name. Member references start with the type specified before colons.

import kotlin.reflect.*

fun printABC() {
    println("ABC")
}

fun double(i: Int): Int = i * 2

class Complex(val real: Double, val imaginary: Double) {
    fun plus(number: Number): Complex = Complex(
        real = real + number.toDouble(),
        imaginary = imaginary
    )
}

fun Complex.double(): Complex =
    Complex(real * 2, imaginary * 2)

fun Complex?.isNullOrZero(): Boolean =
    this == null ||
            (this.real == 0.0 && this.imaginary == 0.0)

class Box<T>(var value: T) {
    fun get(): T = value
}

fun <T> Box<T>.set(value: T) {
    this.value = value
}

fun main() {
    val f1 = ::printABC
    val f2 = ::double
    val f3 = Complex::plus
    val f4 = Complex::double
    val f5 = Complex?::isNullOrZero
    val f6 = Box<Int>::get
    val f7 = Box<String>::set
}

The result type from the function reference is an appropriate KFunctionX, where X indicates the number of parameters. This type also includes type parameters for each function parameter and the result. For instance, the printABC reference type is KFunction0<Unit>. For method references, a receiver is considered another parameter, so the Complex::double type is KFunction1<Complex, Complex>.

// ...

fun main() {
    val f1: KFunction0<Unit> =
        ::printABC
    val f2: KFunction1<Int, Int> =
        ::double
    val f3: KFunction2<Complex, Number, Complex> =
        Complex::plus
    val f4: KFunction1<Complex, Complex> =
        Complex::double
    val f5: KFunction1<Complex?, Boolean> =
        Complex?::isNullOrZero
    val f6: KFunction1<Box<Int>, Int> =
        Box<Int>::get
    val f7: KFunction2<Box<String>, String, Unit> =
        Box<String>::set
}

Alternatively, you can reference methods on concrete instances. These are so-called bounded function references, and they are represented with the same type but without additional parameters for the receiver.

// ...

fun main() {
    val c = Complex(1.0, 2.0)
    val f3: KFunction1<Number, Complex> = c::plus
    val f4: KFunction0<Complex> = c::double
    val f5: KFunction0<Boolean> = c::isNullOrZero
    val b = Box(123)
    val f6: KFunction0<Int> = b::get
    val f7: KFunction1<Int, Unit> = b::set
}

All the specific types representing function references implement the KFunction type, with only one type parameter representing the function result type (because every function must have a result type in Kotlin).

// ...

fun main() {
    val f1: KFunction<Unit> = ::printABC
    val f2: KFunction<Int> = ::double
    val f3: KFunction<Complex> = Complex::plus
    val f4: KFunction<Complex> = Complex::double
    val f5: KFunction<Boolean> = Complex?::isNullOrZero
    val f6: KFunction<Int> = Box<Int>::get
    val f7: KFunction<Unit> = Box<String>::set
    val c = Complex(1.0, 2.0)
    val f8: KFunction<Complex> = c::plus
    val f9: KFunction<Complex> = c::double
    val f10: KFunction<Boolean> = c::isNullOrZero
    val b = Box(123)
    val f11: KFunction<Int> = b::get
    val f12: KFunction<Unit> = b::set
}

Now, what can we do with function references? In a previous book from this series, Functional Kotlin, I showed that they can be used instead of lambda expressions where a function type is expected, like in the example below, where function references are used as arguments to filterNot, map, and reduce.

// ...

fun nonZeroDoubled(numbers: List<Complex?>): List<Complex?> =
    numbers
        .filterNot(Complex?::isNullOrZero)
        .filterNotNull()
        .map(Complex::double)

Using function references where a function type is expected is not “real” reflection because, under the hood, Kotlin transforms these references to lambda expressions. We use function references like this only for our own convenience.

fun nonZeroDoubled(numbers: List<Complex?>): List<Complex?> =
    numbers
        .filterNot { it.isNullOrZero() }
        .filterNotNull()
        .map { it.double() }

Formally, this is possible because types that represent function references, like KFunction2<Int, Int, Int>, implement function types; in this example, the implemented type is (Int, Int) -> Int. So, these types also include the invoke operator function, which lets the reference be called like a function.

fun add(i: Int, j: Int) = i + j

fun main() {
    val f: KFunction2<Int, Int, Int> = ::add
    println(f(1, 2)) // 3
    println(f.invoke(1, 2)) // 3
}

The KFunction by itself includes only a few properties that let us check some function-specific characteristics:

KCallable has many more properties and a few functions. Let’s start with the properties:

KCallable also has two methods that can be used to call it. The first one, call, accepts a vararg number of parameters of type Any? and the result type R, which is the only KCallable type parameter. When we call the call method, we need to provide a proper number of values with appropriate types, otherwise, it throws IllegalArgumentException. Optional arguments must also have a value specified when we use the call function.

import kotlin.reflect.KCallable

fun add(i: Int, j: Int) = i + j

fun main() {
    val f: KCallable<Int> = ::add
    println(f.call(1, 2)) // 3
    println(f.call("A", "B")) // IllegalArgumentException
}

The second function, callBy, is used to call functions using named arguments. As an argument, it expects a map from KParameter to Any? that should include all non-optional arguments.

import kotlin.reflect.KCallable

fun sendEmail(
    email: String,
    title: String = "",
    message: String = ""
) {
    println(
        """
    Sending to $email
    Title: $title
    Message: $message
""".trimIndent()
    )
}

fun main() {
    val f: KCallable<Unit> = ::sendEmail

    f.callBy(mapOf(f.parameters[0] to "ABC"))
    // Sending to ABC
    // Title:
    // Message:

    val params = f.parameters.associateBy { it.name }
    f.callBy(
        mapOf(
            params["title"]!! to "DEF",
            params["message"]!! to "GFI",
            params["email"]!! to "ABC",
        )
    )
    // Sending to ABC
    // Title: DEF
    // Message: GFI

    f.callBy(mapOf()) // throws IllegalArgumentException
}

Parameter references

The KCallable type has the parameters property, with a list of references of type KParameter. This type includes the following properties:

As an example of how the parameters property can be used, I created the callWithFakeArgs function, which can be used to call a function reference with some constant values for the non-optional parameters of supported types. As you can see in the code below, this function takes parameters; it uses filterNot to keep only parameters that are not optional, and it then associates a value with each of them . A constant value is provided by the fakeValueFor function, which for Int always returns 123; for String, it constructs a fake value that includes a parameter name (the typeOf function will be described later in this chapter). The resulting map of parameters with associated values is used as an argument to callBy. You can see how this callWithFakeArgs can be used to execute different functions with the same arguments.

import kotlin.reflect.KCallable
import kotlin.reflect.KParameter
import kotlin.reflect.typeOf

fun callWithFakeArgs(callable: KCallable<*>) {
    val arguments = callable.parameters
        .filterNot { it.isOptional }
        .associateWith { fakeValueFor(it) }
    callable.callBy(arguments)
}

fun fakeValueFor(parameter: KParameter) =
    when (parameter.type) {
        typeOf<String>() -> "Fake ${parameter.name}"
        typeOf<Int>() -> 123
        else -> error("Unsupported type")
    }

fun sendEmail(
    email: String,
    title: String,
    message: String = ""
) {
    println(
        """
    Sending to $email
    Title: $title
    Message: $message
""".trimIndent()
    )
}
fun printSum(a: Int, b: Int) {
    println(a + b)
}
fun Int.printProduct(b: Int) {
    println(this * b)
}

fun main() {
    callWithFakeArgs(::sendEmail)
    // Sending to Fake email
    // Title: Fake title
    // Message:
    callWithFakeArgs(::printSum) // 246
    callWithFakeArgs(Int::printProduct) // 15129
}

Property references

Property references are similar to function references, but they have a slightly more complicated type hierarchy.

All property references implement KProperty, which implements KCallable. Calling a property means calling its getter. Read-write property references implement KMutableProperty, which implements KProperty. There are also specific types like KProperty0 or KMutableProperty1, which specify how many receivers property calls require:

Properties are referenced just like functions: use two colons before their name and an additional class name for member properties. There is no syntax to reference member extension functions or member extension properties; so, to show the KProperty2 example in the example below, I needed to find it in the class reference, which will be described in the next section.

import kotlin.reflect.* import kotlin.reflect.full.memberExtensionProperties val lock: Any = Any() var str: String = “ABC” class Box( var value: Int = 0 ) { val Int.addedToBox get() = Box(value + this) } fun main() { val p1: KProperty0 = ::lock println(p1) // val lock: kotlin.Any val p2: KMutableProperty0 = ::str println(p2) // var str: kotlin.String val p3: KMutableProperty1<Box, Int> = Box::value println(p3) // var Box.value: kotlin.Int val p4: KProperty2<Box, *, *> = Box::class .memberExtensionProperties .first() println(p4) // val Box.(kotlin.Int.)addedToBox: Box }

The KProperty type has a few property-specific properties:

KMutableProperty only adds a single property:

Types representing properties with a specific number of receivers additionally provide this property getter’s get function, and mutable variants also provide the set function for this property setter. Both get and set have an appropriate number of additional parameters for receivers. For instance, in KMutableProperty1, the get function expects a single argument for the receiver, and set expects one argument for the receiver and one for a value. Additionally, more specific types that represent properties provide more specific references to getters and setters.

import kotlin.reflect.*

class Box( var value: Int = 0 ) 

fun main() { 
    val box = Box() 
    val p: KMutableProperty1<Box, Int> = Box::value 
    println(p.get(box)) // 0
    p.set(box, 999) 
    println(p.get(box)) // 999
}

#reads #marcinmoskala #kotlin #reflection #jvm