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:
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")Tis a placeholder filled in when the class or function is used. By convention type parameters are single capital letters:Tfor type,KandVfor key and value,Efor 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:
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:
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 TKotlin 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.
A generic class and function
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>()))
}make bold
type Hello
1
2 items, first is 3.5
0 items, first is nullLine-by-line explanation
- Line 1One Stack class works for any type T.
- Line 12Stack<String> fixes T to String; pushing a number would be a compile error.
- Line 18The same class, used for Ints.
- Line 23An empty list needs its type written, because there's nothing to infer it from.
Upper bounds
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)))
}19
plum
null
6.0
3.75Line-by-line explanation
- Line 1T must be Comparable, which makes the > comparison possible.
- Line 8T must be a Number, so toDouble is available for Int, Double, Long and so on.
- Line 12Strings compare alphabetically, so "plum" is the largest.
Variance and reified types
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 })
}Tom, Luna
2
[Rex]Line-by-line explanation
- Line 5introduce expects List<Animal>.
- Line 7reified keeps T available at run time, so filterIsInstance can check each item's type.
- Line 11A List<Cat> is accepted, because List is declared List<out E>: it only produces items.
- 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>
fun addDog(animals: MutableList<Animal>) = animals.add(Dog("Rex"))
val cats = mutableListOf(Cat("Tom"))
addDog(cats) // type mismatch: it would put a Dog among Catsfun introduce(animals: List<Animal>) { } // read-only List accepts List<Cat>Checking a generic type at run time
if (x is List<String>) { } // error: cannot check for instance of erased typeif (x is List<*> && x.all { it is String }) { }Using features a type might not have
fun <T> biggest(a: T, b: T) = if (a > b) a else b // error: T might not be comparablefun <T : Comparable<T>> biggest(a: T, b: T) = if (a > b) a else bKotlin 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.
Complete the declaration of a generic class that holds one item of any type.
class Box<>(val item: T)
Where does a generic function declare its type parameter?
What does this print?
fun <T> twice(x: T): List<T> = listOf(x, x)
println(twice("ha"))
What does T : Comparable<T> mean?
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.
What problem do generics solve?
- Writing code that works with many types while keeping type safety
- Slow loops
- Null errors
- Formatting text
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
What does
outon 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
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
What does
reifiedrequire?- A data class
- A sealed class
- An inline function
- A var parameter