Reading — step 1 of 7
Learn
Kotlin in Action devotes an entire chapter to generics and variance. The vocabulary — in, out, * — looks small but unlocks the most subtle parts of the type system. Without variance you can't safely express List<Cat> as a List<Animal>.
Basic generics (review)
fun <T> singleton(x: T): List<T> = listOf(x)
class Box<T>(val value: T)
T is a type parameter. Multiple bounded constraints with where:
fun <T> max(items: List<T>): T?
where T : Comparable<T> {
return items.maxOrNull()
}
The variance problem
Suppose Cat : Animal. Is List<Cat> a List<Animal>?
open class Animal
class Cat : Animal()
fun feedAll(list: List<Animal>) { /* ... */ }
val cats: List<Cat> = listOf(Cat(), Cat())
feedAll(cats) // does this work?
In Kotlin, YES — because List<T> is covariant in T (declared with out). In Java, NO — List<T> is invariant by default.
The issue: if MutableList<Cat> were a MutableList<Animal>, you could add(Dog()) to it — heap pollution. So mutation breaks subtyping.
out — covariance ("producer")
out T says T is only PRODUCED (returned), never CONSUMED (taken as input):
interface Source<out T> {
fun get(): T // produces T — OK
// fun set(x: T) // would CONSUME — not allowed with out
}
val intSource: Source<Int> = ...
val anyySource: Source<Any> = intSource // OK — covariant
Kotlin's read-only List<out T> is declared this way — you can read but not write, so subtyping is safe.
in — contravariance ("consumer")
in T says T is only CONSUMED, never PRODUCED:
interface Sink<in T> {
fun put(x: T) // consumes T — OK
// fun get(): T // would PRODUCE — not allowed with in
}
val anySink: Sink<Any> = ...
val intSink: Sink<Int> = anySink // OK — contravariant (reversed)
If you can dump any Animal into a sink, you can certainly dump a Cat. Subtyping is reversed.
The PECS rule (from Java, applies here)
Producer Extends, Consumer Super. In Kotlin: out for producers, in for consumers.
fun <T> copy(source: Source<out T>, sink: Sink<in T>) {
sink.put(source.get())
}
Source produces (out). Sink consumes (in). Now copy<Animal>(catSource, anySink) works because of the variance.
Use-site variance with in / out
If you can't change a class's declaration, you can specify variance at the USE site:
fun copyAll(source: List<out Animal>, dest: MutableList<in Animal>) {
for (x in source) dest.add(x)
}
Like Java's ? extends and ? super. Less common in Kotlin because most stdlib types already declare variance correctly.
Star projection: <*>
"I don't know or care about T":
fun printAll(list: List<*>) {
for (x in list) println(x)
}
Like Java's List<?>. Reads-only as Any?. Useful for utility code that doesn't depend on the element type.
reified — runtime access to T
Normally generics are erased — T::class doesn't compile. With inline + reified, T survives:
inline fun <reified T> Any.castOrNull(): T? = this as? T
val x: Any = "hi"
val s: String? = x.castOrNull<String>() // works
Common mistakes
- Using out where you write to T — compile error. out only allows reading.
- Misreading variance — in/out describe the role of T in the API, not how you use the value.
- Forgetting <out T> on read-only collection types — Kotlin's stdlib does this for you; rolling your own, remember to declare variance.
- Star projection where you need a real T —
List<*>reads asAny?. If you need typed reads, use a generic function with bound T. - inline everything — bloat. Inline only when reified is needed or the function is small + hot.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…