Kotlin Reflection: Type references
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.