In programming language theory, variance describes the relationship between subtyping of simple types and subtyping of respective complex types. For instance, variance answers the question of whether Array<String> is a subtype of Array<Object> when String is a subtype of Object. In Kotlin the answer is no, because Kotlin arrays are invariants, in other words, Array subtyping does not corresponding with the subtyping relationship of the parameter types. On the other hand, Java arrays are covariants, meaning that String[] is a subtype of Object[] because String is a subtype of Object.

While covariance means simple type and the corresponding complex type maintains subtyping in same direction, contravariance means subtyping works in the opposite direction. An example of contravariance is function argument. Suppose we have that two function types. The first one is String -> String (meaning input is String and output is also String) and second one is Object -> String. We can say that the second function type is a subtype of the first function type. Because if a function can take as input Object, then it can take as input String, while the reverse is not always true. In this case the function type is contravariant in its argument type.

A formal definition of covariance and contravariance is:

given type order \(\leq\), a type construct \(I\) is covariant if \(I(A) \leq I(B)\) for all \(A \leq B\).

given type order \(\leq\), a type construct \(I\) is contravariant if \(I(A) \leq I(B)\) where \(A \geq B\).

As mentioned before arrays are invariants in Kotlin, there are however ways to declare generic classes so that they become covariant or contravariant. The out modifier in the below example makes the standard library class List covariant in type E.

interface List<out E> : Collection<E> {...}

fun test(strs: List<String>) {
    val anys: List<Any> = strs // without the out modiifer this would fail at compile time.
}

More specifically out ensures that the type E could only be used in out position of methods. This means the generic type parameter E is only used for retrieving content, not modifying the object. For example:

interface Example<out E> {
    fun consume(input: E) // compilation error
    fun produce(): E // ok
}

This restriction makes the generic class safe as a covariant. Kotlin List meets this requirement because it is immutable. On the other hand, Kotlin Array cannot be made an invariant because it is a modifiable collection. With array if we accept that Array<String> is subtype of Array<Any>, we can run into messy scenarios like below:

fun mess(strs: Array<String>) {
    val Array<Any> anys = strs // if we don't have compile time error here, we will run into runtime error in the next line.
    anys[0] = 1
}

The immutable List<Any> class does not have this problem because it will never be modified with Any object. (Well it will not be modified at all.)

In similar spirit we can make a generic class contravariant with the modifier in.

interface Consumer<in T> { }

fun test(consumeAny: Consumer<Any>) {
    val consumeStr: Consumer<String> = consumeAny // a consumer that can take any type can also take string type.
}

In the above examples we use the in/out modifiers when the class is defined. This is called declaration-site variance. It is also possible to use these modifiers outside of class definition. For instance,

fun getFirst(arr: Array<out Any>): Any {
    return arr[0]
}

fun test(strs: Array<String>) {
    getFirst(strs) // only works when with out modifier in first line.
}

This is called use-site variance. The out modifier ensures that within the scope of getFirst, type Any is only used to retrieve content of arr, and thus arr will not be written with Any type.