Generics and variance

Kotlin · Advanced · 14 min

Variance and reified generics explain library signatures and let you write type-safe, reusable APIs.

Generic classes and functions work much like Java's: class Box<T>(val value: T), fun <T> List<T>.second(): T. Upper bounds restrict type arguments: fun <T : Comparable<T>> maxOf(a: T, b: T); several bounds use where T : A, T : B.

Variance decides when Box<Cat> can be used as Box<Animal>. Kotlin uses declaration-site variance:

  • out T (covariant): the class only produces T. Kotlin's List<out E> is read-only, so a List<String> is a List<Any>.
  • in T (contravariant): the class only consumes T, like Comparable<in T>. A Comparator<Any> can compare strings.
  • No modifier: invariant. MutableList<String> is not a MutableList<Any>, otherwise you could add an Int to it.

The compiler enforces that out types appear only in output positions and in types only in inputs. Use-site variance (fun copy(from: Array<out Any>, to: Array<Any>)) handles classes that aren't variant themselves. A star projection List<*> means "a list of some unknown type": safe to read as Any?, not to write.

On the JVM, type arguments are erased at runtime, so x is List<String> isn't possible. Reified type parameters on inline functions keep the type: inline fun <reified T> Any.isA() = this is T. That's how filterIsInstance<T>() works.

Example

open class Animal(val name: String)
class Cat(name: String) : Animal(name)

class Producer<out T>(private val item: T) { fun get(): T = item }
class Consumer<in T> { fun accept(x: T) = println("accepted $x") }

fun <T : Comparable<T>> maxOf3(a: T, b: T, c: T): T = maxOf(a, maxOf(b, c))

inline fun <reified T> List<Any>.only(): List<T> = filterIsInstance<T>()

fun names(xs: List<*>) = xs.map { it.toString() }   // star projection

fun main() {
    val cats: Producer<Cat> = Producer(Cat("Tom"))
    val animals: Producer<Animal> = cats             // OK: covariant
    println(animals.get().name)                      // Tom

    val anyConsumer = Consumer<Any>()
    val strConsumer: Consumer<String> = anyConsumer  // OK: contravariant
    strConsumer.accept("hi")                         // accepted hi
    println(maxOf3(3, 9, 4))                         // 9
    println(listOf(1, "a", 2.5, "b").only<String>()) // [a, b]
    println(names(listOf(1, null)))                  // [1, null]
    // val bad: MutableList<Any> = mutableListOf<String>()  // error: invariant
}

Remember

  • out = producer (covariant), in = consumer (contravariant)
  • List<out E> is covariant; MutableList<E> is invariant
  • Generics are erased on the JVM; inline + reified keeps the type
  • List<*> reads as Any? but can't be written to

Check yourself

  1. Why can a List<String> be assigned to List<Any> but a MutableList<String> not to MutableList<Any>?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/generics.html