Menu
Coddy logo textTech

Variance With in and out

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 40 of 57.

A List<String> can be passed where a List<Any> is expected, because a List only hands elements out. A MutableList<String> cannot: code that sees it as MutableList<Any> could add an Int to a list of strings:

fun describeAll(items: List<Any>) = items.joinToString { it::class.simpleName ?: "?" }
fun addNumber(items: MutableList<Any>) { items.add(42) }

Inside main:

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // error: MutableList<String> is not a MutableList<Any>

Output:

String, String
[x, 42]

out T makes a generic class covariant: it only produces T values, as return types, and never takes them in. Then a Producer<Cat> counts as a Producer<Animal>:

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

class Producer<out T>(private val item: T) {
    fun get(): T = item
}

fun greet(p: Producer<Animal>) = "hello ${p.get().name}"

Inside main:

val cats: Producer<Cat> = Producer(Cat("Tom"))
println(greet(cats))

Output:

hello Tom

in T makes it contravariant: it only consumes T values, as parameters. A consumer that handles any Animal can stand in for a consumer of Cat:

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

interface Feeder<in T> {
    fun feed(animal: T): String
}
class AnimalFeeder : Feeder<Animal> {
    override fun feed(animal: Animal) = "fed ${animal.name}"
}

fun feedCats(cats: List<Cat>, feeder: Feeder<Cat>) = cats.map { feeder.feed(it) }

Inside main:

println(feedCats(listOf(Cat("Tom"), Cat("Kit")), AnimalFeeder()))

Output:

[fed Tom, fed Kit]

The compiler enforces the direction: an out type parameter cannot be the type of a method parameter, and an in one cannot be returned. The rule of thumb is producer out, consumer in. The standard library does the same: List<out E>, Comparator<in T>. Without either keyword a type parameter is invariant, as in MutableList<E>, which both reads and writes:

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // fine: T goes out
    // fun set(x: T) { }                 // error: T is declared out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // fine: T comes in
    // fun last(): T                     // error: T is declared in
}

Variance can also be written where a type is used. MutableList<in Int> accepts any mutable list that can take Int values, such as a MutableList<Number>, and MutableList<out Number> only allows reading:

fun fillWith(target: MutableList<in Int>, n: Int) {
    for (i in 1..n) target.add(i)
}
fun total(source: MutableList<out Number>) = source.sumOf { it.toDouble() }

Inside main:

val numbers = mutableListOf<Number>(0.5)
fillWith(numbers, 3)
println(numbers)
println(total(mutableListOf(1, 2, 3)))

Output:

[0.5, 1, 2, 3]
6.0
challenge icon

Challenge

Easy

Two interfaces move items around: Source<out T> produces items with next() (returning null when it is empty) and Sink<in T> consumes them with accept(item). They are supplied. Write ListSource<T>(items), which returns the list's items one by one and then null; Collector<T>, which keeps accepted items in a read-only items list; and Counter, a Sink<Any> that counts everything it accepts. Then write pipe(from, vararg to), which moves every item of the source into every sink and returns how many items it moved.

The supplied code pipes a line of words into a Collector<String> and a line of numbers into a Collector<Number>, both times also into one Counter. That works because of out and in: a ListSource<Int> is a Source<Number>, and a Sink<Any> is a Sink<String>. It prints the counts, both collections, counted: 7 and after the end: null.

Your code goes in ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt and Sink.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val words = ListSource(input[0].split(" "))
    val ints = ListSource(input[1].split(" ").map { it.toInt() })
    val counter = Counter()
    val allWords = Collector<String>()
    val numbers = Collector<Number>()
    println("words moved: " + pipe(words, allWords, counter))
    println("numbers moved: " + pipe(ints, numbers, counter))
    println(allWords.items)
    println(numbers.items)
    println("counted: ${counter.count}")
    println("after the end: ${words.next()}")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground