Kotlin's in and out keywords

2022-10-23


I’ve written a lot of Kotlin code over the past three years, but I didn’t get Kotlin’s in and out keywords… until yesterday. Here I’ll record my understanding so hopefully I don’t forget how they work. I’m basing this on Kotlin’s documentation on generics.

in and out on classes

in and out can appear in two different places and have slightly different meanings depending on the context. First, they can appear on a type variable in a class definition:

interface Producer<out T> {
  fun produceT(): T
}

In this case, out T is telling the compiler that methods on Source only return values of type T. Methods on Source are prohibited from taking arguments of type T. The following is a compiler error:

interface Producer<out T> {
    fun consumeT(t: T) // Type parameter T is declared as 'out' but occurs in 'in' position in type T
}

(Side note: Why couldn’t the Kotlin compiler figure out that T is only returned from methods on Source and automatically infer out? Maybe there are situations where we explicitly don’t want T to be out.)

Conversely, in T tells the compiler that methods on Source only take values of type T as arguments:

interface Consumer<in T> {
    fun consumeT(t: T) // No type error

    fun produceT(): T // Type parameter T is declared as 'in' but occurs in 'out' position in type T
}

Why do we need in and out in this context? Kotlin’s documentation on generics gives a good set of examples explaining why these keywords are useful. In brief:

For a class Producer<out T>, Kotlin will treat Producer<Derived> as a subclass of Producer<Base>. For a class Consumer<in T>, Kotlin will treat Consumer<Base> as a superclass of Consumer<Derived>. By default, without the in and out keywords, Producer<Base> and Producer<Derived> are unrelated classes.

This is useful because sometimes we want to treat the return value of a method on Producer<Derived> as a Base. Or we want to pass a Derived into a method on Consumer<Base> that takes a Base.

in and out on variable and argument types

Some classes need to have a mix of methods that take arguments of type T and that return a value of type T. An example from Kotlin’s generics documentation:

class Array<T>(vararg elements: T) {
    operator fun get(index: Int): T { ... }
    operator fun set(index: Int, value: T) { ... }
}

This is where it comes in handy that we can use in and out in a second place: on type variables when specifying the type of a method argument or just of a value. We can do something like:

val array: Array<out String> = Array("hello", "world")
array.get(0) // No type error
array.set(0, "cool" /* Type mismatch, required: Nothing, found: String */)

Or:

val array: Array<in String> = Array("hello", "world")
val result = array.get(0) // result has type Any?
array.set(0, "cool") // No type error

Again this is nice because Array<out String> is a subtype of Array<Any>, while Array<in String> is a supertype of Array<SomeHypotheticalSubTypeOfString>.