A look at Kotlin in-out type variance — Kotlin generics

May 17, 2020 • 10 min read

Kotlin is not new for Android development, and if you haven't used it in your projects yet, I'll recommend you should! (Why? I'll cover that in another blog post).

...

While declaring generics, you could've come across in and out keywords. The most common places should be a custom OnClickListener and in some function arguments. It took a while for me to figure out what they actually mean, but they were simpler than I thought.

# Variance

Wildcard types are one of the most tricky parts of Java type system. The docs says, "Kotlin doesn't have Wildcard types. Instead, it has two other things: declaration-site variance and type projections". We'll only focus on declaration-site variance here.

# Covariant

Let's consider the Collection interface in Java,

// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

The wildcard type argument ? extends E means that this method accepts a collection of objects of E or a subtype of E, but not E itself. This means that we can safely read the E items from collections but cannot write, since we don't know what type of objects E complies to.

The Collection<String> is a subtype of Collection<? extends Object>. This wildcard with an extends-bound makes the type covariant.

# Contravariance

If you have a collection that can only take items from it, then declaring a Collection<String> and reading Objects is completely fine.

// Java
List<String> strings = new ArrayList<>();
Object get(int index) {
    return strings.get(index); // OK!
}

Similarly, if you can only put items in a collection, it's OK to use a Collection<Object> and put Strings into it.

// Java
List<Object> objectList = new ArrayList<>();
objectList.add("John"); // OK!

In Java, we have List<? super String> which is a supertype of List<Object>. This is called contravariance. You can only call methods that take String as an argument, like add(String), but if you try to get String in List<T>, you'll get Object instead of String.

// Java
interface Collection<E> ... {
  default boolean removeIf(Predicate<? super E> filter) {
    ...
  }
}

From the docs

Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers", and proposes the following mnemonic:

PECS stands for Producer-Extends, Consumer-Super.

# Declaration-site variance

In Kotlin, there are simple ways to explain the variance to the compiler. This is called declaration-site variance. To put it out simply in a table,

Covariant Contravariance
Java <? extends> <? super>
Kotlin out in

# out modifier

Suppose we have an interface of type T, which only produces T and never consumes, we use the out keyword.

// Kotlin
interface Source<out T> {
    nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // OK!
    // ...
}

A class C is producer of type parameter Ts but not a consumer of Ts.

# in modifier

Conversly, an interface of type T that only consumes T but never produces, we use the in keyword. The best example is Comparable interface of Kotlin.

interface Comparable<in T> {
    fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    val y: Comparable<Double> = x // OK!
}

A class C is consumer of type parameter Ts but not a producer of Ts.

The in/out modifier is called declaration-site variance, because they are provided at the type paramater declaration site.

The in and out words are now self explanatory. The mnemonic mentioned above in the 'from the docs' tip is not needed here and one can rephrase it as:

Consumer in, Producer out!

...

But some class can't be restricted to only produce Ts or consume Ts. This comes under a whole different topic called type projections which must be covered in another blog post 😉

Abarajithan
Last updated: 9/9/2023, 8:28:10 AM