Skip to main content

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)
}
Open in Kotlin Playground

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)
}
Open in Kotlin Playground

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"))
}
Open in Kotlin Playground

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)
}
Open in Kotlin Playground

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)
}
Open in Kotlin Playground

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)
}
Open in Kotlin Playground

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"))
}
Open in Kotlin Playground

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"))
}
Open in Kotlin Playground
Memory aid

Producer out, consumer in. If a type both produces and consumes the same parameter, invariance is often necessary.

Interview questions​

  1. What does T preserve that Any would lose?
  2. Why can List<Dog> be assigned to List<Animal>?
  3. How would covariance corrupt a MutableList<Dog>?
  4. What does out prevent an API from doing?
  5. What can safely be read from List<*>?