article banner

Kotlin Reflection: Type references

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

In the previous parts, we covered referencing classes, functions, and properties. This part focuses on referencing types and shows a practical example of how type references can be used to generate a random value for a specified type. This part ends with sections about transforming Kotlin Reflection API elements to Java Reflection API elements and breaking encapsulation with reflection.

It’s time to discuss type references of type KType. Types should not be confused with classes. Variables and parameters have types, not classes. Types can be nullable and can have type arguments1.

Examples of types and classes. This image was first published in my book Kotlin Essentials.

Relations between classes, types and objects. This image was first published in my book Kotlin Essentials.

To directly reference a type, we use the typeOf function. Note that this function holds information about nullability and type arguments.

import kotlin.reflect.KType import kotlin.reflect.typeOf fun main() { val t1: KType = typeOf<Int?>() println(t1) // kotlin.Int? val t2: KType = typeOf<List<Int?>>() println(t2) // kotlin.collections.List<kotlin.Int?> val t3: KType = typeOf<() -> Map<Int, Char?>>() println(t3) // () -> kotlin.collections.Map<kotlin.Int, kotlin.Char?> }

KType is a simple class with only three properties: isMarkedNullable, arguments, and classifier.

// Simplified KType definition interface KType : KAnnotatedElement { val isMarkedNullable: Boolean val arguments: List<KTypeProjection> val classifier: KClassifier? }

The isMarkedNullable property is simplest; it returns true if this type is marked as nullable in the source code.

import kotlin.reflect.typeOf fun main() { println(typeOf<Int>().isMarkedNullable) // false println(typeOf<Int?>().isMarkedNullable) // true }

Property arguments provide type arguments of this type, so the List<Int> type will have a single argument of type Int, and Map<Long, Char> will have two type arguments Long and Char. The type of these type arguments is KTypeProjection, which is a data class that includes type and a potential variance modifier, therefore Box<out String> has one type argument: out String.

// Simplified KTypeProjection definition data class KTypeProjection( val variance: KVariance?, val type: KType? )
import kotlin.reflect.typeOf class Box<T> fun main() { val t1 = typeOf<List<Int>>() println(t1.arguments) // [kotlin.Int] val t2 = typeOf<Map<Long, Char>>() println(t2.arguments) // [kotlin.Long, kotlin.Char] val t3 = typeOf<Box<out String>>() println(t3.arguments) // [out kotlin.String] }

Finally, we have the classifier property, which is a way for a type to reference the associated class. Its result type is KClassifier?, which is a supertype of KClass and KTypeParameter. KClass represents a class or an interface. KTypeParameter represents a generic type parameter. The type classifier can be KTypeParameter when we reference generic class members. Also, classifier returns null when the type is not denotable in Kotlin, e.g., an intersection type.

import kotlin.reflect.* class Box<T>(val value: T) { fun get(): T = value } fun main() { val t1 = typeOf<List<Int>>() println(t1.classifier) // class kotlin.collections.List println(t1 is KType) // true println(t1 is KClass<*>) // false val t2 = typeOf<Map<Long, Char>>() println(t2.classifier) // class kotlin.collections.Map println(t2.arguments[0].type?.classifier) // class kotlin.Long val t3 = Box<Int>::get.returnType.classifier println(t3) // T println(t3 is KTypeParameter) // true }
// KTypeParameter definition interface KTypeParameter : KClassifier { val name: String val upperBounds: List<KType> val variance: KVariance val isReified: Boolean }

Type reflection example: Random value

To show how we can use type reference, we will implement the ValueGenerator class with a randomValue method that generates a random value of a specific type. We will specify two variants of this function: one expecting a type as a reified type argument, and another that expects a type reference as a regular argument.

class ValueGenerator( private val random: Random = Random, ) { inline fun <reified T> randomValue(): T = randomValue(typeOf<T>()) as T fun randomValue(type: KType): Any? = TODO() }

When implementing the randomValue function, a philosophical problem arises: if a type is nullable, what is the probability that our random value is null? To solve this problem, I added a configuration that specifies the probability of a null value.

import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.typeOf class RandomValueConfig( val nullProbability: Double = 0.1, ) class ValueGenerator( private val random: Random = Random, val config: RandomValueConfig = RandomValueConfig(), ) { inline fun <reified T> randomValue(): T = randomValue(typeOf<T>()) as T fun randomValue(type: KType): Any? = when { type.isMarkedNullable -> randomNullable(type) // ... else -> error("Type $type not supported") } private fun randomNullable(type: KType) = if (randomBoolean(config.nullProbability)) null else randomValue(type.withNullability(false)) private fun randomBoolean(probability: Double) = random.nextDouble() < probability }

Now we can add support for some other basic types. Boolean is simplest because it can be generated using nextBoolean from Random. The same can be said about Int, but 0 is a special value, so I decided to specify its probability in the configuration as well.

import kotlin.math.ln import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.typeOf class RandomValueConfig( val nullProbability: Double = 0.1, val zeroProbability: Double = 0.1, ) class ValueGenerator( private val random: Random = Random, val config: RandomValueConfig = RandomValueConfig(), ) { inline fun <reified T> randomValue(): T = randomValue(typeOf<T>()) as T fun randomValue(type: KType): Any? = when { type.isMarkedNullable && randomBoolean(config.nullProbability) -> null type == typeOf<Boolean>() -> randomBoolean() type == typeOf<Int>() -> randomInt() // ... else -> error("Type $type not supported") } private fun randomInt() = if (randomBoolean(config.zeroProbability)) 0 else random.nextInt() private fun randomBoolean() = random.nextBoolean() private fun randomBoolean(probability: Double) = random.nextDouble() < probability }

Finally, we should generate strings and lists. The biggest problem here is size. If we used random numbers as a size for random collections, these collections would be huge. A random value for a type like List<List<List<String>>> could literally consume all our memory, not to mention the fact that the readability of such objects would be poor. But we should also make it possible for our function to generate a big collection or a string as this might be an edge case we need in some unit tests. I believe that the sizes of the collections and strings we use in real projects are described with exponential distribution: most of them are rather short, but some are huge. The exponential distribution is parametrized with a lambda value. I made this parameter configurable. By default, I decided to specify the exponential distribution parameter for strings as 0.1, which makes our function generate an empty string with around 10% probability, and a string longer than 5 characters with around 55% probability. For lists, I specify the default parameter as 0.3, which means that lists will be empty with around 25% probability, and they have only 16% probability of having a size greater than 5.

import kotlin.math.ln import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.withNullability import kotlin.reflect.typeOf class RandomValueConfig( val nullProbability: Double = 0.1, val zeroProbability: Double = 0.1, val stringSizeParam: Double = 0.1, val listSizeParam: Double = 0.3, ) class ValueGenerator( private val random: Random = Random, val config: RandomValueConfig = RandomValueConfig(), ) { inline fun <reified T> randomValue(): T = randomValue(typeOf<T>()) as T fun randomValue(type: KType): Any? = when { type.isMarkedNullable -> randomNullable(type) type == typeOf<Boolean>() -> randomBoolean() type == typeOf<Int>() -> randomInt() type == typeOf<String>() -> randomString() type.isSubtypeOf(typeOf<List<*>>()) -> randomList(type) // ... else -> error("Type $type not supported") } private fun randomNullable(type: KType) = if (randomBoolean(config.nullProbability)) null else randomValue(type.withNullability(false)) private fun randomString(): String = (1..random.exponential(config.stringSizeParam)) .map { CHARACTERS.random(random) } .joinToString(separator = "") private fun randomList(type: KType) = List(random.exponential(config.listSizeParam)) { randomValue(type.arguments[0].type!!) } private fun randomInt() = if (randomBoolean(config.zeroProbability)) 0 else random.nextInt() private fun randomBoolean() = random.nextBoolean() private fun randomBoolean(probability: Double) = random.nextDouble() < probability companion object { private val CHARACTERS = ('A'..'Z') + ('a'..'z') + ('0'..'9') + " " } } private fun Random.exponential(f: Double): Int { return (ln(1 - nextDouble()) / -f).toInt() }

Let's use what we’ve implemented so far to generate a bunch of random values:

fun main() { val r = Random(1) val g = ValueGenerator(random = r) println(g.randomValue<Int>()) // -527218591 println(g.randomValue<Int?>()) // -2022884062 println(g.randomValue<Int?>()) // null println(g.randomValue<List<Int>>()) // [-1171478239] println(g.randomValue<List<List<Boolean>>>()) // [[true, true, false], [], [], [false, false], [], // [true, true, true, true, true, true, true, false]] println(g.randomValue<List<Int?>?>()) // [-416634648, null, 382227801] println(g.randomValue<String>()) // WjMNxTwDPrQ println(g.randomValue<List<String?>>()) // [VAg, , null, AIKeGp9Q7, 1dqARHjUjee3i6XZzhQ02l, DlG, , ] }

Added 1 as a seed value to Random to produce predictable pseudo-random values for demonstration purposes.

We could push this project much further and make it support many more types. We could also generate random class instances thanks to the classifier property and constructors. Nevertheless, let's stop where we are.

Kotlin and Java reflection

On Kotlin/JVM, we can use the Java Reflection API, which is similar to the Kotlin Reflection API but is primarily designed to be used with Java, not Kotlin, but both type hierarchies are similar. We can transform between these two hierarchies using extension properties accessible on Kotlin/JVM. For example, we can use the java property to transform KClass to Java Class, or we can use javaMethod to transform KFunction to Method. Similarly, we can transform Java Reflection classes to Kotlin Reflection classes using extension properties which start with the kotlin prefix. For example, we can use the kotlin property to transform Class to KClass, and the kotlinFunction property to transform Method to KFunction.

import java.lang.reflect.* import kotlin.reflect.* import kotlin.reflect.jvm.* class A { val a = 123 fun b() {} } fun main() { val c1: Class<A> = A::class.java val c2: Class<A> = A().javaClass val f1: Field? = A::a.javaField val f2: Method? = A::a.javaGetter val f3: Method? = A::b.javaMethod val kotlinClass: KClass<A> = c1.kotlin val kotlinProperty: KProperty<*>? = f1?.kotlinProperty val kotlinFunction: KFunction<*>? = f3?.kotlinFunction }

Breaking encapsulation

Using reflection, we can call elements we should not have access to. Each element has a specified accessibility parameter that we can check using the isAccessible property in the Kotlin and Java Reflection APIs. The same property can be used to change elements’ accessibility. When we do that, we can call private functions or operate on private properties. Of course, you should avoid doing this if not absolutely necessary.

import kotlin.reflect.* import kotlin.reflect.full.* import kotlin.reflect.jvm.isAccessible class A { private var value = 0 private fun printValue() { println(value) } override fun toString(): String = "A(value=$value)" } fun main() { val a = A() val c = A::class // We change value to 999 val prop = c.declaredMemberProperties .find { it.name == "value" } as? KMutableProperty1<A, Int> prop?.isAccessible = true prop?.set(a, 999) println(a) // A(value=999) println(prop?.get(a)) // 999 // We call printValue function val func: KFunction<*>? = c.declaredMemberFunctions .find { it.name == "printValue" } func?.isAccessible = true func?.call(a) // 999 }

Summary

Reflection is a powerful tool used by many libraries to implement functionalities specific to code elements’ definitions. We shouldn’t use reflection too often in our applications as it is considered heavy, but it is good to know how to use it because it offers possibilities that cannot be substituted with anything else.

1:

I explained the differences between classes and types in the The beauty of Kotlin’s type system chapter of Kotlin Essentials.