Kotlin Generics

Write classes and functions that work with any type safely, using type parameters, upper bounds, variance with in and out, star projections and reified types.

Lesson 20 of 43Advanced18 min read

Every time you write List<String> or Map<String, Int>, you're using generics. The type in angle brackets tells Kotlin what the collection holds, so it can stop you putting a number into a list of names and you never need to cast what you take out.

Generics let you write one piece of code that works with many types while keeping full type safety. A Box<T> can hold an apple or an email; a firstOrDefault<T>() function works for lists of anything. Without generics, you'd either duplicate code for each type or give up the compiler's checks.

In this lesson you'll write generic classes and functions, restrict which types are allowed with upper bounds, understand variance (out and in), which explains why a List<Cat> can be used as a List<Animal>, and use reified type parameters to check types at run time.

Generic classes and functions

Add type parameters in angle brackets:

Kotlin
class Box<T>(val item: T) {
    fun describe() = "Box of $item"
}

fun <T> pickFirst(a: T, b: T): T = a

val numberBox = Box(42)            // Box<Int>, inferred
val word: String = pickFirst("hi", "bye")
  • T is a placeholder filled in when the class or function is used. By convention type parameters are single capital letters: T for type, K and V for key and value, E for element.
  • Kotlin usually infers the type from the arguments, so you rarely write Box<Int>(42).
  • For generic functions, the type parameter goes before the function name: fun <T> name(...).

Upper bounds and where clauses

An upper bound restricts which types are allowed, which in turn lets you use their features:

Kotlin
fun <T : Comparable<T>> largest(items: List<T>): T {
    var best = items[0]
    for (item in items) if (item > best) best = item
    return best
}

T : Comparable<T> means "any type that can be compared with itself", which makes > available. largest(listOf(3, 9, 4)) and largest(listOf("pear", "apple")) both work. For several constraints, use a where clause: fun <T> save(x: T) where T : Named, T : Serializable.

Variance: out and in

Is a List<Cat> a List<Animal>? For read-only List, yes: you can only take Cats out, and every Cat is an Animal. Kotlin declares this with out: interface List<out E>. A type parameter marked out is only ever produced (returned), never consumed.

For MutableList, no: if a MutableList<Cat> could be treated as a MutableList<Animal>, someone could add a Dog to it. So MutableList<E> has no out.

The opposite, in, marks a type that's only consumed (passed in), as in Comparator<in T>: something that can compare any Animal can certainly compare Cats. A helpful memory aid is: producer out, consumer in.

Star projections, erasure and reified

When you don't know or care about the type argument, use a star projection: List<*> is a list of something, which you can read as Any?.

Generic types are erased at run time: a List<String> and a List<Int> are both just List. So if (x is List<String>) isn't allowed. Inline functions can mark a type parameter as reified, which keeps the real type available inside the function:

Kotlin
inline fun <reified T> List<Any>.filterType(): List<T> = filter { it is T }.map { it as T }

The standard library's filterIsInstance<T>() works exactly like this.

Kotlin Generics Syntax

Generics syntax

class Box<T>(val item: T)
fun <T> firstOf(list: List<T>): T? = list.firstOrNull()
fun <T : Comparable<T>> maxOf(a: T, b: T) = if (a > b) a else b
fun <T> f(x: T) where T : CharSequence, T : Comparable<T> { }
interface Source<out T> { fun next(): T }
interface Sink<in T> { fun put(item: T) }
fun printAll(list: List<*>) { }
inline fun <reified T> Any.isA() = this is T

Kotlin Generics Summary

  • Type parameters in angle brackets let classes and functions work with any type safely.
  • Generic functions declare their type parameter before the name: fun <T> f(x: T).
  • Upper bounds like T : Comparable<T> restrict types and unlock their features.
  • out marks types that are only produced, in marks types only consumed: producer out, consumer in.
  • Types are erased at run time; inline functions with reified type parameters can check them.

Kotlin Generics Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A generic class and function

Kotlin
class Stack<T> {
    private val items = mutableListOf<T>()
    fun push(item: T) { items.add(item) }
    fun pop(): T? = items.removeLastOrNull()
    fun peek(): T? = items.lastOrNull()
    val size get() = items.size
}

fun <T> describe(items: List<T>): String = "${items.size} items, first is ${items.firstOrNull()}"

fun main() {
    val undo = Stack<String>()
    undo.push("type Hello")
    undo.push("make bold")
    println(undo.pop())
    println(undo.peek())

    val numbers = Stack<Int>()
    numbers.push(7)
    println(numbers.size)

    println(describe(listOf(3.5, 1.0)))
    println(describe(listOf<String>()))
}
Output
make bold
type Hello
1
2 items, first is 3.5
0 items, first is null
Line-by-line explanation
  1. Line 1One Stack class works for any type T.
  2. Line 12Stack<String> fixes T to String; pushing a number would be a compile error.
  3. Line 18The same class, used for Ints.
  4. Line 23An empty list needs its type written, because there's nothing to infer it from.
Example 2

Upper bounds

Kotlin
fun <T : Comparable<T>> largest(items: List<T>): T? {
    if (items.isEmpty()) return null
    var best = items[0]
    for (item in items) if (item > best) best = item
    return best
}

fun <T : Number> total(items: List<T>): Double = items.sumOf { it.toDouble() }

fun main() {
    println(largest(listOf(4, 19, 7)))
    println(largest(listOf("pear", "apple", "plum")))
    println(largest(emptyList<Int>()))
    println(total(listOf(1, 2, 3)))
    println(total(listOf(1.5, 2.25)))
}
Output
19
plum
null
6.0
3.75
Line-by-line explanation
  1. Line 1T must be Comparable, which makes the > comparison possible.
  2. Line 8T must be a Number, so toDouble is available for Int, Double, Long and so on.
  3. Line 12Strings compare alphabetically, so "plum" is the largest.
Example 3

Variance and reified types

Kotlin
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
class Dog(name: String) : Animal(name)

fun introduce(animals: List<Animal>) = animals.joinToString { it.name }

inline fun <reified T> List<Animal>.only(): List<T> = filterIsInstance<T>()

fun main() {
    val cats: List<Cat> = listOf(Cat("Tom"), Cat("Luna"))
    println(introduce(cats))

    val pets: List<Animal> = listOf(Cat("Tom"), Dog("Rex"), Cat("Luna"))
    println(pets.only<Cat>().size)
    println(pets.only<Dog>().map { it.name })
}
Output
Tom, Luna
2
[Rex]
Line-by-line explanation
  1. Line 5introduce expects List<Animal>.
  2. Line 7reified keeps T available at run time, so filterIsInstance can check each item's type.
  3. Line 11A List<Cat> is accepted, because List is declared List<out E>: it only produces items.
  4. Lines 14 to 15The same function filters Cats or Dogs depending on the type argument.

Common Kotlin Generics Mistakes

Expecting MutableList<Cat> to be a MutableList<Animal>

Don't
fun addDog(animals: MutableList<Animal>) = animals.add(Dog("Rex"))
val cats = mutableListOf(Cat("Tom"))
addDog(cats)                  // type mismatch: it would put a Dog among Cats
Do
fun introduce(animals: List<Animal>) { }   // read-only List accepts List<Cat>

Checking a generic type at run time

Don't
if (x is List<String>) { }    // error: cannot check for instance of erased type
Do
if (x is List<*> && x.all { it is String }) { }

Using features a type might not have

Don't
fun <T> biggest(a: T, b: T) = if (a > b) a else b    // error: T might not be comparable
Do
fun <T : Comparable<T>> biggest(a: T, b: T) = if (a > b) a else b

Kotlin Generics in Real Projects

Generics in practice

Every collection, Result<T>, Flow<T> and StateFlow<T> in Kotlin is generic. Android architecture code often has generic base classes like Repository<T> or UiState<T>, and networking libraries such as Retrofit use generics to turn JSON into the right data class. Reading signatures with out, in and reified becomes easy once you've written a few generic functions yourself.

Kotlin Generics FAQ

What does T mean in Kotlin?

T is a type parameter: a placeholder for a real type that's filled in when the class or function is used. In fun <T> firstOrDefault(xs: List<T>, d: T): T, calling it with a List<Int> makes T stand for Int. The name is only a convention; E for elements and K and V for map keys and values are common too.

Can a generic type parameter be null in Kotlin?

Yes, by default. A plain <T> has an implicit upper bound of Any?, so T can be a nullable type such as String?. If your function must never receive null, declare <T : Any>, and passing a nullable type becomes a compile error.

What is the difference between List<Any> and List<*>?

List<Any> is a list declared to hold any non-null objects. List<*> is a list of some specific type you don't know, such as a List<String> passed in from elsewhere. You can read items from either, but from List<*> they come out as Any?, and you can't add to a MutableList<*> at all, because Kotlin can't check what's safe to put in.

Why can't I check is List<String> in Kotlin?

Generic type arguments are erased when the code runs on the JVM, so at run time a List<String> is just a List. Kotlin refuses the check rather than give a misleading answer. Check is List<*> and then the items, or use filterIsInstance<String>(), which tests each item individually.

What does reified mean in Kotlin?

reified is a modifier for a type parameter of an inline function. Because an inline function's body is copied into each call site, the compiler knows the real type there, so you can use T in is T checks or T::class. It can't be used on classes or on ordinary, non-inline functions.

Kotlin Generics Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration of a generic class that holds one item of any type.

class Box<>(val item: T)
Multiple choiceeasy

Where does a generic function declare its type parameter?

Predict the outputmedium

What does this print?

fun <T> twice(x: T): List<T> = listOf(x, x)
println(twice("ha"))
Multiple choicemedium

What does T : Comparable<T> mean?

Multiple choicehard

Why can a List<Cat> be passed where List<Animal> is expected, but not a MutableList<Cat> for MutableList<Animal>?

Kotlin Generics Quiz

5 questions to check what you learned.

  1. What problem do generics solve?

    • Writing code that works with many types while keeping type safety
    • Slow loops
    • Null errors
    • Formatting text
  2. What does List<*> mean?

    • A list of anything that can be changed
    • A list of some unknown type, read as Any?
    • An empty list
    • A list of lists
  3. What does out on a type parameter mean?

    • The type is private
    • The type is optional
    • It must be a subclass
    • Values of that type are only produced (returned), never consumed
  4. Why can't you write x is List<String>?

    • Strings can't be in lists
    • is only works with classes
    • It's allowed
    • Generic type arguments are erased at run time
  5. What does reified require?

    • A data class
    • A sealed class
    • An inline function
    • A var parameter