๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

Kotlin Variance at Depth

Declaration-site variance puts the out or in marker on the class header itself. Every use of the class then follows the direction the header declares.

A Box read through a wider reference

Box declares out on its header. A Box of String can sit in a Box of Any reference and still hand its value back.

class Box<out T>(private val value: T) {
    fun read(): T = value
}

fun main() {
    val stringBox: Box<String> = Box("hello")
    val anyBox: Box<Any> = stringBox
    println(anyBox.read())
}

A Sink written through a narrower view

Sink declares in on its header. A Sink of Any can be viewed as a Sink of String, and strings written through that view land in the same sink.

class Sink<in T> {
    private val stored = mutableListOf<Any?>()
    fun put(item: T) {
        stored.add(item)
    }
    fun size(): Int = stored.size
    fun last(): Any? = stored.last()
}

fun main() {
    val anySink = Sink<Any>()
    val stringSink: Sink<String> = anySink
    stringSink.put("data")
    stringSink.put("more")
    println(anySink.size())
    println(anySink.last())
}

One reader for every Box

The printBox function takes a Box of Any. Covariance lets it accept a Box of String and a Box of Int with no extra code.

class Box<out T>(private val value: T) {
    fun read(): T = value
}

fun printBox(box: Box<Any>) {
    println(box.read())
}

fun main() {
    printBox(Box("first"))
    printBox(Box(2))
}

An invariant list refuses the swap

MutableList carries no variance marker, so it is invariant. This program assigns a MutableList of String to a MutableList of Any variable, and the compiler rejects it.

fun main() {
    val names: MutableList<String> = mutableListOf("a", "b")
    val anything: MutableList<Any> = names
    println(anything.size)
}

Example

class Box<out T>(private val value: T) {
    fun read(): T = value
}

class Sink<in T> {
    private val stored = mutableListOf<Any?>()
    fun put(item: T) {
        stored.add(item)
    }
    fun size(): Int = stored.size
}

fun main() {
    val wordBox: Box<Any> = Box("signal")
    println(wordBox.read())
    val tray = Sink<Any>()
    val wordTray: Sink<String> = tray
    wordTray.put("alpha")
    wordTray.put("beta")
    println(tray.size())
}

Key points

  • The out marker on a class header makes the class covariant in its type parameter.
  • A Box of a narrow type can be read through a Box of a wider type.
  • The in marker on a class header makes the class contravariant in its type parameter.
  • A Sink of a wide type can be written through a Sink of a narrower type.
  • The marker lives in one place, on the header, rather than at each use.
  • A class with no marker stays invariant, so its type arguments must match exactly.

In this example, the Box of String is read through a Box of Any reference because the header declares out. The Sink of Any is filled through a Sink of String view because the header declares in. Both directions come from the two class headers.

Note

The compiler checks that an out type is only produced and an in type is only consumed.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Kotlin Playground

Exercise

What does the out marker on a class header declare?