Understanding Kotlin limitations for type parameter positions
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:
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.