Generics in Kotlin — Variances

Co-Variance

A co-variant class is a generic class where Producer<A> is subtype of Producer<B> if A is subtype of B. i.e. subtype is preserved.

Eg : Producer<Cat> is subtype of Producer<Animal>

To declare the class to be co-variant on certain type parameter, we put out before the type parameter

interface Producer<out T> {
produce() : T
}

It is important that T only exists in out positions in all the exposed methods (i.e. public and protected).

in and out positions

We will understand this with an example

when we pass herd of cats to feedAll(), though the cats are animals, it does not feed herd of cats.
Make Herd co-variant on type T (which is any type of Animal)
If I add another method to Herd where we take in a parameter of type T, you get error saying “you have declared T to be out but you are using it in an in position”
But you do not see that error if I make that method private
constructor parameters are neither in or out
but those constructor parameters should not be var. with var you expose a setter which exposes you type as ‘in’ position
Make the ‘var’ private, error goes away. Since the position rules are applicable only for external APIs.

There are 2 APIs for list in Kotlin — List (which is immutable) and MutableList

Since List is immutable, which means the type parameter always occurs in out position (you only read values from an immutable list), you can see that List is co-variant on its type parameter

List interface in Kotlin

Contra-variance

Co-variance preserves subtype. Contra-variance reverses the subtype. Consumer<A> is a subtype of Consumer<B> if A is a supertype of B.

For Eg : Comparator<Any> can be used to compare Strings as well. Meaning

List.sortedWith : If you need to perform comparisons on objects of a certain type, you can use a comparator that handles either that type or any of its supertypes.

val anyComparator = Comparator<Any> { a,b -> a.hashCode() — b.hashCode() }
val strings : List<String> = ....
strings.sortedWith(anyComparator)

Comparator<Any> is a subtype of Comparator<String> where Any is the supertype of String

To summarize,

MutableList has T occurring in both ‘in’ and ‘out’ position.

Class or interface can be co-variant on one type parameter and contra-variant on another. Classic example is Function interface

Higher order function enumerateCats() can use a lambda that enumerate any Animal

Use-site variance

The ability to specify variance annotations on class declarations is convenient because the annotations apply to all places where the class is used. This is called declaration-site variance. In Java, variances are handled a bit differently — using ? extends and ? super . In Java, every time you use a type with a type parameter, you can specify whether this type parameter can be replaced with subtypes or supertypes. This is called use-site variance. But specifying the variance annotations once, and having clients not to think about them, is concise and elegant.

MutableList are not co-variant or contra-variant — because they can both produce and consume values of types specified by type parameters. But we could use MutableList in a producer only context or consumer only context. To support such cases, Kotlin supports use-site variance.

Event though both source and destination are invariant on T, source is used only for reading and destination only for writing
Source element type should be a subtype of the destination’s element type. i.e. copyDateModified(strings, anyItems)
or you can use this elegant approach by making use of variance annotations (Ideally we would just use List for source)

Here, the source is called type projection. source is not a MutableList but a projected (restricted) one.

Star Projection

MutableList<*> means that we don’t have any information about the generic argument. Note that it is not the same as MutableList<Any?> , later is the list which has elements of Any type.

Type mismatch error
MutableList<*> acts as MutableList<out Any?>

MutableList<*> acts as MutableList<out Any?>. When you know nothing about the type of element, it is safe to get elements of Any? type, but it is not safe to put elements into the list. The java equivalent of MutableList<*> is MutableList<?>

Let’s look at a case where Star projections lead you to a trap.

FieldValidator<*> only works with out projection. So you are forced to case a validator explicitly to a type you need.

This could be written better by localizing the unsafe code.

Now you have a type-safe API. All unsafe logic is hidden. The compiler forbids you to use an incorrect validator because the Validators object will always give you correct validator implementation.

Not a blogger. I mostly move my well-written notes here.