A look at Kotlin in-out type variance — Kotlin generics
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 Object
s 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 String
s 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 T
s but not a consumer of T
s.
# 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 T
s but not a producer of T
s.
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 T
s or consume T
s. This comes under a whole different topic called type projections
which must be covered in another blog post 😉