Understanding Kotlin limitations for type parameter positions

Marcin Moskala
Kt. Academy
Published in
5 min readMay 28, 2018

--

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).

Do you need a Kotlin workshop? Visit our website to see what we can do for you.

To be up-to-date with great news on Kt. Academy, subscribe to the newsletter, observe Twitter and follow us on medium.

Click the 👏 to say “thanks!” and help others find this article.

Note that if you hold the clap button, you can leave more claps.

--

--

Kt. Academy creator, co-author of Android Development with Kotlin, author of open-source libraries, community activist. http://marcinmoskala.com/