Generics and variance, progressively explained
Learn this in order:
Generic function -> Generic class -> Mutable-list safety problem
-> covariance (out) -> contravariance (in) -> star projection
1. Generic function
A generic function preserves a caller's type without separate String, Int, and User versions.
fun <T> second(items: List<T>): T? = items.getOrNull(1)
fun main() {
val number: Int? = second(listOf(10, 20, 30))
val word: String? = second(listOf("Kotlin", "Android"))
println(number)
println(word)
}
T is a placeholder chosen by the caller/compiler. Passing List<Int> produces Int?, not Any?.
2. Generic class
class Box<T>(val value: T)
fun main() {
val text: Box<String> = Box("Android")
val count: Box<Int> = Box(42)
println(text.value.uppercase())
println(count.value + 1)
}
3. Constraints
A bound makes an algorithm's required capability compile-time visible.
fun <T : Comparable<T>> maximum(a: T, b: T): T = if (a >= b) a else b
fun main() {
println(maximum(10, 20))
println(maximum("Kotlin", "Android"))
}
4. Why List<Dog> can be read as List<Animal>
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
fun main() {
val dogs: List<Dog> = listOf(Dog("Pixel"))
val animals: List<Animal> = dogs
println(animals.first().name)
}
Reading a Dog as an Animal is safe. Kotlin declares read-only List as covariant.
5. Why MutableList cannot do the same
open class Animal
class Dog : Animal()
class Cat : Animal()
fun main() {
val dogs: MutableList<Dog> = mutableListOf(Dog())
// val animals: MutableList<Animal> = dogs // compile error
// animals.add(Cat()) // would put Cat into dogs
println(dogs.size)
}
MutableList both returns and accepts T. It is invariant: MutableList<Dog> is not a subtype of MutableList<Animal>.
6. out T: producer
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
interface Source<out T> { fun next(): T }
class DogSource : Source<Dog> { override fun next() = Dog("Pixel") }
fun main() {
val animals: Source<Animal> = DogSource()
println(animals.next().name)
}
out means values of T safely come out of the contract. The compiler restricts unsafe input positions.
7. in T: consumer
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
interface Sink<in T> { fun accept(value: T) }
class AnimalSink : Sink<Animal> {
override fun accept(value: Animal) = println(value.name)
}
fun main() {
val dogSink: Sink<Dog> = AnimalSink()
dogSink.accept(Dog("Milo"))
}
A sink capable of accepting every Animal can accept a Dog. This reversed safe relationship is contravariance.
8. Star projection
List<*> means the element type is unknown. You may read elements as Any?, but cannot assume String or Int.
fun describe(items: List<*>) {
for (item: Any? in items) println(item)
}
fun main() {
describe(listOf(1, 2))
describe(listOf("Kotlin", "Android"))
}
Producer out, consumer in. If a type both produces and consumes the same parameter, invariance is often necessary.
Interview questions
- What does
Tpreserve thatAnywould lose? - Why can
List<Dog>be assigned toList<Animal>? - How would covariance corrupt a
MutableList<Dog>? - What does
outprevent an API from doing? - What can safely be read from
List<*>?