Kotlin variance modifiers impose limitations on type parameters usage. Covariant type parameter (with out modifier) can’t be used on public in positions, and contravariant type parameter (with in modifier) can’t be used on public out positions. But why were such restriction introduced?

Let’s understand it.

Variance modifiers

We have already published an article that explains deeply variance modifiers. It can be found here. It can be summarized shortly:

When a generic type is invariant, like class Box<T> , there is no relation between any Box<SomeType> and Box<AnotherType> . So there is no relation between Box<Number> and Box<Int> .

When a generic type is covariant, like class Box<out T> , when A is a subtype of B then Box<A> is a subtype of Box<B> . So Box<Int> is a subtype of Box<Number> .

When a generic type is contravariant, like class Box<in T> , when A is a subtype of B then Box<B> is a subtype of Box<A> . So Box<Number> is a subtype of Box<Int> .

Short summary:

Taken from Kt. Academy cheat sheet.

Review limitations

Although Kotlin introduced some limitations on type parameters with variance modifiers usage. Following class is fully correct:

class SomeClass<T> {

var t: T? = null



fun functionReturningT(): T? = t



fun functionAcceptingT(t: T) {}



private fun privateFunctionReturningT(): T? = t



private fun privateFunctionAcceptingT(t: T) {}

}

Although it will not compile if we introduce any variance modifier:

class SomeClass<out T> {

var t: T? = null // Error



private var pt: T? = null



fun functionReturningT(): T? = t



fun functionAcceptingT(t: T) {} // Error



private fun privateFunctionReturningT(): T? = t



private fun privateFunctionAcceptingT(t: T) {}

} class SomeClass<in T> {

var t: T? = null // Error



private var pt: T? = null



fun functionReturningT(): T? = t // Error



fun functionAcceptingT(t: T) {}



private fun privateFunctionReturningT(): T? = t



private fun privateFunctionAcceptingT(t: T) {}

}

As you can see, covariant type can’t be used on public methods as a parameter type and it cannot be used for public read-write properties. Read only are fine because they expose only out position:

class SomeClass<out T> {

val t: T? = null



private var pt: T? = null



fun functionReturningT(): T? = t



private fun privateFunctionReturningT(): T? = t



private fun privateFunctionAcceptingT(t: T) {}

}

Contravariance cannot be used on as a return type from methods and on all methods (getter visibility must be the same as property visibility).

Example problem

To understand the problem behind this limitations, think of Java arrays. They were covariant and in the same time, they allow setting value (in position). As a result, you can invoke following code which is fully correct from the compilation point of view, but will always result in runtime error:

// Java

Integer[] ints = { 1,2,3 };

Object[] objects = ints;

objects[2] = "AAA";

What happened there? We up-casted array and then set down-casted type and boom! We have an error. How does it relate to position?

Positions and typing

In-positions and out-positions have some default casting contract. See following type hierarchy:

When we need to pass Dog to in-position, every subtype is accepted as well:

fun takeDog(dog: Dog) {} takeDog(Dog())

takeDog(Puppy())

takeDog(Hund())

When we take Dog from out position, accepted values are Dog and all supertypes:

fun makeDog(): Dog = Dog() val any: Any = makeDog()

val animal: Animal = makeDog()

val wild: Wild = makeDog()

Notice that once element is on in or out position, different type of casting is default and it cannot be stopped.

This is what happened in our array example. Covariance allowed up-casting, and in position allowed down-casting. Using this two mechanism together we can cast to everything. Similarly with contravariance and out position. Together can help developer cast any type to any other. The only problem is that if an actual type cannot be casted this way then we have a runtime error.

The only way to prevent this is to prohibit connection of public in-positions and contravariance, and public our-positions and variance. This is why Kotlin has this limitation. Kotlin also solved array problem by making all arrays invariant. It is another example that Kotlin is much safer language than Java (see this presentation).