What you'll learn
Generics let you write code that works with any type while staying fully type-safe — the reason List<String> knows it holds strings. This module covers generic classes and functions, constraints, and Kotlin's clean approach to variance.
By the end you'll be able to:
- Write generic classes and functions
- Constrain type parameters
- Understand variance with
outandin
Generic classes & functions
A generic type takes a type parameter in angle brackets — conventionally T. Inside, T stands for whatever type the caller uses, and the compiler infers it from the arguments:
class Box<T>(val item: T) { // T is a type parameter
fun get(): T = item
}
val stringBox = Box("Hello") // T inferred as String
val intBox = Box(42) // T inferred as Int
println(stringBox.get()) // Hello
println(intBox.get()) // 42Constraints (bounded types)
Sometimes T can't be anything — you need it to have certain capabilities. An upper bound like <T : Comparable<T>> restricts T to types that satisfy it, so you can use those capabilities (here, comparing with >):
// <T : Comparable<T>> constrains T to types that can be compared
fun <T : Comparable<T>> larger(a: T, b: T): T =
if (a > b) a else b
println(larger(3, 7)) // 7
println(larger("apple", "pear")) // pearVariance: out & in
Here's a subtle question: should a List<Cat> be usable where a List<Animal> is expected? Intuitively yes — every cat is an animal. Kotlin controls this with variance markers on the type parameter:
out T — producer
You only read T out (like a List). Lets a List<Cat> be used where a List<Animal> is expected (covariance).
in T — consumer
You only write T in (like a comparator). The reverse relationship (contravariance).
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
// List<out T> is covariant, so a List<Cat> works where List<Animal> is expected
fun printNames(animals: List<Animal>) {
animals.forEach { println(it.name) }
}
val cats: List<Cat> = listOf(Cat("Milo"), Cat("Felix"))
printNames(cats) // works!Producer out, consumer in
out when a type only produces values you read (safe to be covariant), and in when it only consumes values you write. Kotlin's declaration-site variance (you mark it once on the class) is far cleaner than Java's wildcards scattered at every use.Note
value is T normally; the reified trick from Module 12 (inside an inline function) is the workaround. The star projection <*> means "some type, but I don't know or care which."Recap & quick check
Key takeaways
- Generics provide type-safe, reusable code; a type parameter <T> is filled in by the caller.
- Generic classes and functions both use type parameters; the compiler usually infers them.
- An upper bound like <T : Comparable<T>> constrains T so you can use those capabilities.
- Variance: 'out' (producer, covariant — read only) and 'in' (consumer, contravariant — write only).
- Kotlin uses declaration-site variance (marked once on the class), cleaner than Java's wildcards; generics are erased at runtime.
Quick check
1. What is the main benefit of generics?
2. What does <T : Comparable<T>> do?
3. What does the 'out' variance marker mean?
4. How does Kotlin's variance differ from Java's?
5. Are Kotlin generics available at runtime?
Excellent — generics unlock type-safe, reusable code. Next up: Module 23 — Delegation & Delegated Properties, the finale of Phase 4.