Variance & Reified Types
Variance answers a subtle question: if Cat is a Animal, is
Box<Cat> a Box<Animal>? Kotlin lets you declare the answer with out and
in.
Covariance with out
out means the type parameter is only produced (returned), never consumed.
Then Source<Cat> is a Source<Animal>:
interface Source<out T> {
fun next(): T // T only appears in output position
}
val cats: Source<Cat> = ...
val animals: Source<Animal> = cats // OK because of `out`
Kotlin’s read-only List<out E> is covariant, which is why a List<String> can
be used where a List<Any> is expected.
Contravariance with in
in means the type parameter is only consumed (passed in), never produced.
Then Sink<Animal> is a Sink<Cat>:
interface Sink<in T> {
fun put(item: T) // T only appears in input position
}
val anySink: Sink<Animal> = ...
val catSink: Sink<Cat> = anySink // OK because of `in`
Remember PECS: Producer-out, Consumer-in. If a generic type only hands
values out, mark it out; if it only takes values in, mark it in.
Use-site variance (star projection)
When you do not care about the exact type argument, use *:
fun printAll(list: List<*>) { // a List of something
list.forEach { println(it) }
}
reified type parameters
Normally generic types are erased at runtime, so you cannot write T::class or
is T. An inline function with a reified type parameter keeps the type
available:
inline fun <reified T> List<*>.firstOfType(): T? =
firstOrNull { it is T } as? T
val mixed = listOf(1, "two", 3.0, "four")
mixed.firstOfType<String>() // "two"
Because the function is inline, the compiler substitutes the real type, so is
T works.
Exercises
-
Explain why
MutableList<T>cannot be markedout(hint: it both produces and consumesT). -
Write
inline fun <reified T> Iterable<*>.countOfType(): Intreturning how many elements are instances ofT.Solution
inline fun <reified T> Iterable<*>.countOfType(): Int =
count { it is T }
</details>
This solution is in examples/core/part4/ and is tested by CI.
Previous: Generics · Next: Part 5: Functional & Advanced