Generics are a powerful means of code reuse, and are widely used in both Java and Kotlin. We've touched on generics before, but haven't discussed their intricacies yet. In this chapter, we'll recap why we need them, see how they work in Kotlin including advanced features, and how they provide additional safety in the type system.
Imagine for a moment that we don't have a list collection provided to us by the JDK or the Kotlin Standard Library, and have to implement our own. Without generics, we'd have two choices:
-
Implement a list type for each type of object that we want to store in lists. For example, a
StringListclass would take aStringas the parameter of itsaddmethod, and return aStringfrom itsgetmethod. This is convenient and safe to use, but whenever we want to create a list of a new type, we have to implement a new list class that can store that specific type.class StringList { fun add(string: String) { ... } fun get(index: Int): String { ... } } val stringList = StringList() stringList.add("testing") val test: String = stringList.get(0)
-
Create a single
Listtype that stores aListofAny(orObject) instances, which solves the issue of having to create new types all the time. However, we now have to remember that we stored, say,Stringinstances in a given list, then remember to only putStringinstances in it, and finally cast them back toStringwhen we read them from the list, so that we can actually use them.class List { fun add(t: Any) { ... } fun get(index: Int): Any { ... } } val stringList = List() stringList.add("testing") // remember, Strings only! val test: String = stringList.get(0) as String // hope it's a String
It probably doesn't need further explanation that both of these approaches are remarkably inconvenient and error-prone.
With generics, we can create a List<T> class, which uses the generic type parameter T in its implementation, such as for the parameter of add and the return type of get. Then, if we instantiate a List<String>, the type parameter is fulfilled by the concrete type argument String, and we'll only be able to add String instances to our list, and get any objects out of it with the String type as well. Safe and convenient!
class List<T> {
fun add(t: T) { ... }
fun get(index: Int): T { ... }
}
val stringList = List<String>()
stringList.add("testing")
val test: String = stringList.get(0)Under the hood, on the JVM, we're essentially performing the second scenario described above - but the compiler is helping us out with a lot of the type checking and casting.
Similarly to generic classes, we can create generic functions, by adding type parameters to them. These allow us to, for example, define a forEach function that can work on a List<T>, regardless of what the actual T type is:
inline fun <T> List<T>.forEachItem(actions: (T) -> Unit) {
for (element in this) actions(element)
}The T type parameter is declared right after the fun keyword. Calling this on a List<Person> will substitute the type parameter with Person thanks to type inference, and give us a parameter of the Person type in our lambda:
people.forEachItem { person: Person ->
println(person.name)
}It's also possible to explicitly pass a type parameter to a function, if it can't be inferred from context, for example a function like this:
fun <T> create(): T { /* create an instance of T somehow... */ }
create<Person>()Type parameters are usually single letters in Kotlin. If you look into the Standard Library, you'll see plenty of
T,R, andC. These are great if they're being used in relatively standard ways (Tbeing whatever single generic parameter,Ra return type parameter,Ca collection of some kind), but they can get confusing otherwise. Don't be afraid to use generic type names such asItemorElementif it makes your code easier to read.
You can specify upper bounds for generic type parameters, if you have requirements for the type arguments. For example, you might want to create a function that accepts numbers, prints them as a Double, and then returns them with their original type:
fun <N : Number> printNumber(number: N): N {
println(number.toDouble())
return number
}
val l: Long = printNumber(23L)An upper bound that you might want to use a lot is, surprisingly, Any. If you don't specify an upper bound for your functions, the default will actually be Any?, which allows any nullable type to be substituted as well!
fun <T> testEquality(t1: T, t2: T): Boolean {
return t1.equals(t2)
// ^ e: Only safe (?.) or non-null asserted (!!.) calls
// are allowed on a nullable receiver of type T
}Using Any as an upper bound will only allow subtypes of Any, which are all non-nullable types.
fun <T : Any> testEquality(t1: T, t2: T): Boolean {
return t1.equals(t2)
}Note that specifying a type parameter such as
T : Anystill lets you use theT?type within the implementation of the function, if you need to.
By default, you can specify a single constraint on a type parameter when you declare it. But what if we needed, for example, a type parameter that implements both of these interfaces?
interface Writer {
fun write(str: String)
}
interface Reader {
fun read(): String
}If you need multiple upper bounds, you can specify them at the very end of the function signature, with the where keyword:
fun <T> readWriteStuff(t: T)
where T : Reader, T : Writer {
val str = t.read()
t.write(str)
}While the language doesn't support intersection types (a type that's defined as a subtype of multiple types), these kinds of combined constraints can give you type parameters which can behave a lot like an intersection type.
Unit being a proper type comes in handy with generics - let's say we have a Task<T> type like this:
interface Task<T> {
fun execute(): T
}If we need an instance that returns no result when it's done, in Java we couldn't express that as Task<void>, because void is not a type, just a keyword which signifies the special case of functions that don't return a result. Instead, we'd use the Void type, which is the typesystem-equivalent of void, to create a Task<Void>.
In Kotlin, we can use the same Unit in both of these places to describe the lack of meaningful data, so we'd end up with simply Task<Unit>, with an execute function that returns the Unit type as usual.
The interaction of generics and inheritance is rather interesting. For simple types, we know that they can be used wherever their supertypes are expected. What about generic types?
Let's create a Box, which lets you insert and then retrieve a generic type of Snack:
interface Box<T : Snack> {
fun insert(snack: T)
fun take(): T?
}Let's create some specific snacks as well:
interface Snack
class Pretzel : Snack
class Donut : SnackThe million-dollar question, then: given that a Pretzel is a Snack, does it follow that a Box<Pretzel> is a Box<Snack>?
Let's see if we can use a Box<Pretzel> where a Box<Snack> is expected. We'll create a function that takes a Box<Snack> as a parameter, and uses its functionality. It first empties the entire box, and then inserts a new Snack in it (specifically, a Donut).
fun testBox(box: Box<Snack>) {
// Empty the box
while (true) {
val snack: Snack = box.take() ?: break
println("Removed $snack")
}
// Add a new snack
box.insert(Donut())
}
breakis also an expression, and its return type isNothing.
Now, let's call it with a Box<Pretzel> as its parameter:
val pretzelBox = object : Box<Pretzel> {
override fun insert(snack: Pretzel) { ... }
override fun take(): Pretzel { ... }
}
testBox(pretzelBox)We get an error on the call to testBox:
Type mismatch.
Required: Box<Snack>
Found: Box<Pretzel>
So why are we not allowed to pass in this parameter? Whenever take() is called, our Box<Pretzel> will return a Pretzel?, which is a Snack?. This is all sound.
However, then we want to insert a Donut() into the Box<Snack> (and we could attempt this with any other Snack instance as well). This is a problem, as a Box<Pretzel> only accepts Pretzel instances! This means that a Box<Pretzel> is not a Box<Snack>.
We've seen that a Box<Pretzel> worked fine in the place of a Box<Snack> in certain ways, and not in others. It was fine as long as we only took generic values out of it. If we just remove the problematic method that takes values of T as a parameter, our Box<Pretzel> would be safe to use as a Box<Snack>.
Let's define an interface that only allows taking snacks out of it:
interface Dispenser<out T : Snack> {
fun take(): T?
}Note how we've marked the T type parameter with the out keyword. This keyword ensures that the generic type parameter is only used in out positions, such as the return type of functions or properties. If we used T for a function's parameter inside this interface, in an in position, we'd get a compilation error. In exchange for this restriction, the compiler now knows that a Dispenser<Pretzel> can safely function as a Dispenser<Snack>:
fun testDispenser(dispenser: Dispenser<Snack>) {
// Take a snack from the dispenser
val snack: Snack = requireNotNull(dispenser.take())
println("Grabbing a $snack")
}
val pretzelDispenser = object: Dispenser<Pretzel> {
override fun take(): Pretzel? { ... }
}
testDispenser(pretzelDispenser) // All good!This behavior, marked by the out keyword, is called covariance. A generic type Generic<T> is covariant on its type parameter if X being a subtype of Y also means that a Generic<X> will be a subtype of a Generic<Y>. The subtyping direction is the same for the parameters and the generic types that use them. They vary together, they are covariant.
What about putting snacks into a Box? We've seen that a Box<Pretzel> can't work as a Box<Snack> for these purposes, because a Box<Snack> has to accept any kind of Snack. However, we can spot an inverse relationship here: a Box<Snack> could work where a Box<Pretzel> is needed, as it can accept Pretzel instances!
Of course it won't be true that Box<Snack> is a subtype of Box<Pretzel>. When we want to pick up a Pretzel, we can't get one from a Box<Snack> in a type safe manner, because it might contain other types of snacks, and give us one of those. The same problem as before - we have to split this functionality into a one-way interface:
interface Collector<in T : Snack> {
fun insert(snack: T)
}This time, the type parameter is marked with the in keyword, which restricts its usages to in positions: only as the types of values that the interface receives as a parameter. This tells the compiler that an instance of a Collector may have a more general type argument than what's required.
Case in point, it makes our Collector<Snack> work as a Collector<Pretzel>, just like we wanted it to:
fun testCollector(collector: Collector<Pretzel>) {
collector.insert(Pretzel())
}
val snackCollector = object : Collector<Snack> {
override fun insert(snack: Snack) { ... }
}
testCollector(snackCollector) // OK!This behavior, marked by the in keyword, is called contravariance. A generic type Generic<T> is contravariant on its type parameter if X being a subtype of Y also means that a Generic<Y> will be a subtype of a Generic<X>. The subtyping direction is reversed between the type arguments and the generic types that use them. They vary in the opposite direction, they are contravariant.
With these new interfaces in place, we can easily reintroduce the Box type as the combination of the two interfaces:
interface Box<T : Snack> : Dispenser<T>, Collector<T>The type parameter of Box can neither be covariant nor contravariant, as it uses the T type parameter in both in and out positions. A type like this is an invariant type.
We've seen that reading values from a type with a generic type parameter enables covariance. This is another benefit of having separate
ListandMutableListinterfaces in the Standard Library. While theMutableListinterface is doomed to be invariant,Listis actually covariant. AList<Pretzel>is automatically aList<Snack>, which makes using lists a lot smoother!
There are first-party types that use this exact same approach of splitting a bidirectional interface into two interfaces for variance.
Channelis a good example of this, as it combines theSendChannelandReceiveChannelinterfaces, which are contravariant and covariant, respectively.
See the extras for this chapter for even more advanced use cases for variance in Kotlin. You'll even learn what the Box<*> type means!
Generics and variance also give us an opportunity to use Nothing again. It's a common pattern to define a sealed type that can either hold a generic result, or wrap an error, like this:
sealed interface Result<T : Any>
class Success<T : Any>(val result: T) : Result<T>
class Error<T : Any>(val throwable: Throwable) : Result<T>Note how the result
Ttype can never be nullable here.
If we want to report an error for a call that expects a Result like this, we can do it by creating an Error instance:
fun getData(): Result<String> {
return Error<String>(RuntimeException("oh bother"))
}The way that this Error class works is... interesting. It defines a generic type parameter that it doesn't use - or rather, uses only to conform to its supertype. This class doesn't actually have to be generic to inherit from Result. It doesn't use its generic parameter in any way, and there's no reason why the same Error containing an exception couldn't be a Result that asked for a String and a Result that asked for a Customer at the same time. We can express this like so, thanks to Nothing being a bottom type:
sealed interface Result<T : Any>
class Success<T : Any>(val result: T) : Result<T>
class Error(val throwable: Throwable) : Result<Nothing>If we haven't realized that we needed variance on this class so far, we'd definitely realize it now, as a Result<Nothing> at this point is not a Result<String> or a Result<Customer>, so we can't use Error in the way that we wanted to use it.
Similarly, code like this doesn't work either, because a Result<Employee> is not a Result<Person>:
open class Person
class Employee : Person()
fun getPerson() : Result<Person> {
return Success<Employee>(Employee())
}The issue in both of these cases is that Result is invariant (the default behaviour for a generic parameter, if not specified otherwise), so only exact type arguments can be used. This can be fixed by adding covariance to our Result class, using the out keyword:
sealed interface Result<out T : Any>
class Success<T : Any>(val result: T) : Result<T>
class Error(val throwable: Throwable) : Result<Nothing>Sealed classes for results of operations like this one are used frequently in Kotlin, as a way to avoid having to handle exceptions with
try-catchblocks. Instead, methods using these result types can always return normally, and their results can be checked for success or failure using awhenstatement.
The Standard Library also contains a
Resulttype that's quite similar to the one we've implemented here.
In Java, whenever we use generics, we have to deal with a "feature" of the JVM called type erasure. To oversimplify it: generic types only exist at compile time, and all generic parameters are replaced with the Object type by the time they end up in the bytecode. This differs for example from the C++ implementation of generics, where separate, typed classes get generated for every type parameter that a generic class is used with. (These approaches are the second and first of the painful manual options laid out in the introduction at the beginning of this chapter, respectively).
This type erasure is the reason why we have trouble with...
-
Telling apart a
List<Kitten>and aList<Tiger>at runtime by performinginstanceofchecks on aListinstanceif (list instanceof List<Kitten>) { print("Aww.") // ^ Error: Illegal generic type for instanceof } else if (list instanceof List<Tiger>) { print("Uh-oh.") // ^ Error: Illegal generic type for instanceof }
-
Getting the
.classof a type parameter like we'd do with a regular typeSystem.out.println(String.class); // This works System.out.println(T.class); // ^ Error: Cannot select from a type variable
-
Performing type checks against a type parameter
System.out.println("" instanceof String); // This works System.out.println("" instanceof T); // ^ Error: Class or array expected
So what are reified generics, and how do they help us with these issues in Kotlin? They are, essentially, a clever compile-time trick. When we create a generic function in Kotlin, we can mark its type parameters as reified - but only if we also make the function itself inline. For example:
inline fun <reified T> printType(t: T) {
println("I think '$t' is ${T::class}")
}Accessing the generic parameter's class wouldn't normally be possible on the JVM, however, if we call this function with the reified parameter, it works:
printType("hi") // I think 'hi' is class kotlin.String
printType(Bar()) // I think 'Bar' is class foo.BarHow is this possible? Remember, we can only make type parameters reified if our function is inlined - and that's exactly where the magic happens. When we call an inline function, its body is essentially copied to the call site instead of a function call being performed.
When this happens for a function that has a reified type parameter, all usages of that type parameter in the body get replaced with the concrete type argument that the function was invoked with at the call site. This is the same process that happens to regular parameters during inlining, just applied to type parameters this time.
For example, our previous two calls above compile to these lines (at the bytecode level), with the concrete values of the type parameters inlined:
println("I think '${"hi"}' is ${String::class}")
println("I think '${Bar()}' is ${Bar::class}")... which are perfectly valid calls to perform on these concrete types.
This handy transformation also lets us perform type checks such as x is T if T is reified. For example, this generic function can be easily implemented in Kotlin:
inline fun <reified R> List<*>.filterType(): List<R> {
val result = mutableListOf<R>()
for (element in this) {
if (element is R) {
result.add(element)
}
}
return result
}It's an extension on a List with an unknown (or any) type argument, takes a reified type parameter, and filters the original list to only those elements that are of the type R. Its usage would look like this:
val list = listOf(1, 'o', 992.5233, 2, 25.21, "foo", 17)
println(list.filterType<Double>()) // [992.5233, 25.21]When we invoke this filterType function with the Double type argument, the type check inside it becomes an element is Double check at compile time, which is perfectly valid bytecode.
This function is actually called
filterIsInstance, and it's part of the Kotlin Standard Library.
Kotlin has powerful support for advanced generics, with the help of type constraints, variance, and projections. You don't even really have to keep track of how these things interact in your code - the compiler pays attention to all of this for you, and ensures that you're only performing safe operations.
Nothing can be a useful type when defining generic class hierarchies. Reified generic type parameters let you access the values of type arguments at runtime.
- Illustrated articles on generics by Dave Leeds
- Official documentation



