
KType. Types should not be confused with classes. Variables and parameters have types, not classes. Types can be nullable and can have type arguments[^9_1].
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.
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? }
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 }
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] }
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 }
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() }
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 }
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 }
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 around55% 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() }
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 toRandomto produce predictable pseudo-random values for demonstration purposes.
classifier property and constructors. Nevertheless, let's stop where we are.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 }
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 }