How it works
The explanation, with examples you can run.
Why variance exists
If MutableList<String> were a MutableList<Any>, this would compile and then break:
val strings = mutableListOf("a")// val anys: MutableList<Any> = strings // not allowed, and here is why:// anys.add(42) // a 42 inside a list of Strings// val s: String = strings[1] // ClassCastExceptionSo MutableList<T> is invariant: only exactly MutableList<String> fits. But the read-only List<out E> never accepts an E, only returns them, so treating a List<String> as a List<Any> can't go wrong. Kotlin's standard library declares it out, and the assignment compiles.
Declaring it
interface Source<out T> { fun next(): T } // T only in "out" positionsinterface Sink<in T> { fun put(item: T) } // T only in "in" positions class Logger : Sink<Any> { override fun put(item: Any) = println("log: $item")} fun <T> copy(from: Source<T>, to: Sink<T>, n: Int) = repeat(n) { to.put(from.next()) } fun main() { var i = 0 val numbers = object : Source<Int> { override fun next() = ++i } val anySource: Source<Any> = numbers // fine: out val stringSink: Sink<String> = Logger() // fine: in stringSink.put("hello") // log: hello copy(numbers, Logger(), 2) // log: 1, log: 2 println(anySource.next()) // 3}The compiler checks the declaration: an out T can't appear as a parameter type, an in T can't be returned. A handy reading is producer out, consumer in (Java's PECS, written on the type instead of the call).
copy works with a Sink<Any> where the signature asks for Sink<Int>, because in makes Sink<Any> a subtype of Sink<Int>.
Use-site variance and star projections
When a type is invariant but one function only reads from it, you can project at the use site: fun sum(xs: Array<out Number>) accepts an Array<Int>, and inside it xs[0] = 1.5 is rejected. Array<in Int> is the opposite. A star projection List<*> means "some type I don't know": reading gives Any?, writing to a MutableList<*> isn't allowed.
Bounds and erasure
- Upper bounds:
fun <T : Comparable<T>> max(a: T, b: T). Several bounds usewhere T : CharSequence, T : Comparable<T>. - Nullability: a plain
Tmay be nullable (its bound isAny?);T : Anymakes it non-null.T & Any(definitely non-null) helps when overriding Java methods. - Erasure:
xs is List<String>can't be checked at runtime; onlyxs is List<*>can. The Java path's generics topic covers why. Aninline fun <reified T>gets around it by being copied into each caller.
Variance appears in function types too: (Any) -> Int is usable where (String) -> Number is expected, because parameters are in and results are out.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.