1. Overview

In this article, we’ll be looking at the generic types in the Kotlin language.

They are very similar to those from the Java language, but the Kotlin language creators tried to make them a little bit more intuitive and understandable by introducing special keywords like out and in.

2. Creating Parameterized Classes

Let’s say that we want to create a parameterized class. We can easily do this in Kotlin language by using generic types:

class ParameterizedClass<A>(private val value: A) {

    fun getValue(): A {
        return value
    }
}

We can create an instance of such a class by setting a parameterized type explicitly when using the constructor:

val parameterizedClass = ParameterizedClass<String>("string-value")

val res = parameterizedClass.getValue()

assertTrue(res is String)

Happily, Kotlin can infer the generic type from the parameter type so we can omit that when using the constructor:

val parameterizedClass = ParameterizedClass("string-value")

val res = parameterizedClass.getValue()

assertTrue(res is String)

3. Kotlin out and in Keywords

3.1. The Out Keyword

Let’s say that we want to create a producer class that will be producing a result of some type T. Sometimes; we want to assign that produced value to a reference that is of a supertype of the type T.

To achieve that using Kotlin, we need to use the out keyword on the generic type. It means that we can assign this reference to any of its supertypes. The out value can be only be produced by the given class but not consumed:

class ParameterizedProducer<out T>(private val value: T) {
    fun get(): T {
        return value
    }
}

We defined a ParameterizedProducer class that can produce a value of type T.

Next; we can assign an instance of the ParameterizedProducer class to the reference that is a supertype of it:

val parameterizedProducer = ParameterizedProducer("string")

val ref: ParameterizedProducer<Any> = parameterizedProducer

assertTrue(ref is ParameterizedProducer<Any>)

If the type T in the ParamaterizedProducer class will not be the out type, the given statement will produce a compiler error.

3.2. The in Keyword

Sometimes, we have an opposite situation meaning that we have a reference of type T and we want to be able to assign it to the subtype of T.

We can use the in keyword on the generic type if we want to assign it to the reference of its subtype. The in keyword can be used only on the parameter type that is consumed, not produced:

class ParameterizedConsumer<in T> {
    fun toString(value: T): String {
        return value.toString()
    }
}

We declare that a toString() method will only be consuming a value of type T.

Next, we can assign a reference of type Number to the reference of its subtype – Double:

val parameterizedConsumer = ParameterizedConsumer<Number>()

val ref: ParameterizedConsumer<Double> = parameterizedConsumer

assertTrue(ref is ParameterizedConsumer<Double>)

If the type T in the ParameterizedCounsumer will not be the in type, the given statement will produce a compiler error.

4. Type Projections

4.1. Copy an Array of Subtypes to an Array of Supertypes

Let’s say that we have an array of some type, and we want to copy the whole array into the array of Any type. It is a valid operation, but to allow the compiler to compile our code we need to annotate the input parameter with the out keyword.

This lets the compiler know that the input argument can be of any type that is a subtype of the Any:

fun copy(from: Array<out Any>, to: Array<Any?>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

If the from parameter is not of the out Any type, we will not be able to pass an array of an Int type as an argument:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any?> = arrayOfNulls(3)

copy(ints, any)

assertEquals(any[0], 1)
assertEquals(any[1], 2)
assertEquals(any[2], 3)

4.2. Adding Elements of a Subtype to an Array of Its Supertype

Let’s say that we have the following situation – we have an array of Any type that is a supertype of Int and we want to add an Int element to this array. We need to use the in keyword as a type of the destination array to let the compiler know that we can copy the Int value to this array:

fun fill(dest: Array<in Int>, value: Int) {
    dest[0] = value
}

Then, we can copy a value of the Int type to the array of Any:

val objects: Array<Any?> = arrayOfNulls(1)

fill(objects, 1)

assertEquals(objects[0], 1)

4.3. Star Projections

There are situations when we do not care about the specific type of value. Let’s say that we just want to print all the elements of an array and it does not matter what the type of the elements in this array is.

To achieve that, we can use a star projection:

fun printArray(array: Array<*>) { 
    array.forEach { println(it) }
}

Then, we can pass an array of any type to the printArray() method:

val array = arrayOf(1,2,3) 
printArray(array)

When using the star projection reference type, we can read values from it, but we cannot write them because it will cause a compilation error.

5. Generic Constraints

Let’s say that we want to sort an array of elements, and each element type should implement a Comparable interface. We can use the generic constraints to specify that requirement:

fun <T: Comparable<T>> sort(list: List<T>): List<T> {
    return list.sorted()
}

In the given example, we defined that all elements T needed to implement the Comparable interface. Otherwise, if we will try to pass a list of elements that do not implement this interface, it will cause a compiler error.

We defined a sort function that takes as an argument a list of elements that implement Comparable, so we can call the sorted() method on it. Let’s look at the test case for that method:

val listOfInts = listOf(5,2,3,4,1)

val sorted = sort(listOfInts)

assertEquals(sorted, listOf(1,2,3,4,5))

We can easily pass a list of Ints because the Int type implements the Comparable interface.

5.1. Multiple Upper Bounds

With the angle bracket notation, we can declare at most one generic upper bound. If a type parameter needs multiple generic upper bounds, then we should use separate where clauses for that particular type parameter. For instance:

fun <T> sort(xs: List<T>) where T : CharSequence, T : Comparable<T> {
    // sort the collection in place
}

As shown above, the parameter T must implement the CharSequence and Comparable interfaces at the same time. Similarly, we can declare classes with multiple generic upper bounds:

class StringCollection<T>(xs: List<T>) where T : CharSequence, T : Comparable<T> {
    // omitted
}

6. Generics at Runtime

6.1. Type Erasure

As with Java, Kotlin’s generics are erased at runtime. That is, an instance of a generic class doesn’t preserve its type parameters at runtime.

For example, if we create a Set<String> and put a few strings into it, at runtime we’re only able to see it as a Set.

Let’s create two Sets with two different type parameters:

val books: Set<String> = setOf("1984", "Brave new world")
val primes: Set<Int> = setOf(2, 3, 11)

At runtime, the type information for Set<String> and Set<Int> will be erased and we see both of them as plain Sets. So, even though it’s perfectly possible to find out at runtime that value is a Set, we can’t tell whether it’s a Set of strings, integers, or something else: that information has been erased.

So, how does Kotlin’s compiler prevent us from adding a Non-String into a Set<String>? Or, when we get an element from a Set<String>, how does it know the element is a String?

The answer is simple. The compiler is the one responsible for erasing the type information but before that, it actually knows the books variable contains String elements.

So, every time we get an element from it, the compiler would cast it to a String or when we’re gonna add an element into it, the compiler would type check the input.

6.2. Reified Type Parameters

Let’s have more fun with generics and create an extension function to filter Collection elements based on their type:

fun <T> Iterable<*>.filterIsInstance() = filter { it is T }
Error: Cannot check for instance of erased type: T

The “it is T” part, for each collection element, checks if the element is an instance of type T, but since the type information has been erased at runtime, we can’t reflect on type parameters this way.

Or can we?

The type erasure rule is true in general, but there is one case where we can avoid this limitation: Inline functions. Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.

The body of inline functions is inlined. That is, the compiler substitutes the body directly into places where the function is called instead of the normal function invocation.

If we declare the previous function as inline and mark the type parameter as reified, then we can access generic type information at runtime:

inline fun <reified T> Iterable<*>.filterIsInstance() = filter { it is T }

The inline reification works like a charm:

>> val set = setOf("1984", 2, 3, "Brave new world", 11)
>> println(set.filterIsInstance<Int>())
[2, 3, 11]

Let’s write another example. We all are familiar with those typical SLF4j Logger definitions:

class User {
    private val log = LoggerFactory.getLogger(User::class.java)
    
    // ...
}

Using reified inline functions, we can write more elegant and less syntax-horrifying Logger definitions:

inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)

Then we can write:

class User {
    private val log = logger<User>()

    // ...
}

This gives us a cleaner option to implement logging, the Kotlin way.

6.3. Deep Dive into Inline Reification

So, what’s so special about inline functions so that type reification only works with them? As we know, Kotlin’s compiler copies the bytecode of inline functions into places where the function is called.

Since in each call site, the compiler knows the exact parameter type, it can replace the generic type parameter with the actual type references.

For example, when we write:

class User {
    private val log = logger<User>()

    // ...
}

When the compiler inlines the logger<User>() function call, it knows the actual generic type parameter – User. So instead of erasing the type information, the compiler seizes the reification opportunity and reifies the actual type parameter.

7. Conclusion

In this article, we were looking at the Kotlin Generic types. We saw how to use them out and in keywords properly. We used type projections and defined a generic method that uses generic constraints.

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.

Comments are closed on this article!