article banner

Kotlin Reflection: Method and property references

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

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:

  • isInline: Boolean - true if this function is inline.
  • isExternal: Boolean - true if this function is external.
  • isOperator: Boolean - true if this function is operator.
  • isInfix: Boolean - true if this function is infix.
  • isSuspend: Boolean - true if this is a suspending function.
import kotlin.reflect.KFunction inline infix operator fun String.times(times: Int) = this.repeat(times) fun main() { val f: KFunction<String> = String::times println(f.isInline) // true println(f.isExternal) // false println(f.isOperator) // true println(f.isInfix) // true println(f.isSuspend) // false }

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

  • name: String - The name of this callable as declared in the source code. If the callable has no name, a special invented name is created. Here are some atypical cases:
    • constructors have the name "",
    • property accessors: the getter for a property named "foo" will have the name ""; similarly the setter will have the name "".
  • parameters: List<KParameter> - a list of references to the parameters of this callable. We will discuss parameter references in a dedicated section.
  • returnType: KType - the type that is expected as a result of this callable call. We will discuss the KType type in a dedicated section.
  • typeParameters: List<KTypeParameter> - a list of generic type parameters of this callable. We will discuss the KTypeParameter type in the section dedicated to class references.
  • visibility: KVisibility? - visibility of this callable, or null if its visibility cannot be represented in Kotlin. KVisibility is an enum class with values PUBLIC, PROTECTED, INTERNAL, and PRIVATE.
  • isFinal: Boolean - true if this callable is final.
  • isOpen: Boolean - true if this function is open.
  • isAbstract: Boolean - true if this function is abstract.
  • isSuspend: Boolean - true if this is a suspending function (it is defined in both KFunction and KCallable).
import kotlin.reflect.KCallable operator fun String.times(times: Int) = this.repeat(times) fun main() { val f: KCallable<String> = String::times println(f.name) // times println(f.parameters.map { it.name }) // [null, times] println(f.returnType) // kotlin.String println(f.typeParameters) // [] println(f.visibility) // PUBLIC println(f.isFinal) // true println(f.isOpen) // false println(f.isAbstract) // false println(f.isSuspend) // false }

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:

  • index: Int - the index of this parameter.
  • name: String? - a simple parameter name, or null if the parameter has no name or its name is not available at runtime. Examples of nameless parameters include a this instance for member functions, an extension receiver for extension functions or properties, and parameters of Java methods compiled without debug information.
  • type: KType - the type of this parameter.
  • kind: Kind - the kind of this parameter, which can be one of the following:
    • VALUE for regular parameters.
    • EXTENSION_RECEIVER for extension receivers.
    • INSTANCE for dispatch receivers, so instances needed to make member callable calls.
  • isOptional: Boolean - true if this parameter is optional, therefore it has a default argument specified.
  • isVararg: Boolean - true if this parameter is vararg.

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:

  • Read-only top-level properties implement KProperty0 because their getter can be called without any receiver.
  • Read-only member or extension properties implement KProperty1 because their getter needs a single receiver object.
  • Read-only member extension properties implement KProperty2 because their getter needs two receivers: a dispatch receiver and an extension receiver.
  • Read-write top-level properties implement KMutableProperty0 because their getter can be called without any receiver.
  • Read-write member or extension properties implement KMutableProperty1 because their getter and setter need a single receiver object.
  • Read-write member extension properties implement KMutableProperty2 because their getter and setter need two receivers: a dispatch receiver and an extension receiver.

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<Any> = ::lock println(p1) // val lock: kotlin.Any val p2: KMutableProperty0<String> = ::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:

  • isLateinit: Boolean - true if this property is lateinit.
  • isConst: Boolean - true if this property is const.
  • getter: Getter<V> - a reference to an object representing a property getter.

KMutableProperty only adds a single property:

  • setter: Setter<V> - a reference to an object representing a property setter.

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 }

In the next part, we’ll cover class references and show how to implement object serialization. See you then!