126

Kotlin generic variance modifiers – Kotlin Academy

 6 years ago
source link: https://blog.kotlin-academy.com/kotlin-generics-variance-modifiers-36b82c7caa39?gi=a0f282ca681d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Kotlin generic variance modifiers

Published in
6 min readJan 18, 2018

Even for developers with experience, generics are sometimes confusing. Let’s finally make it simple and clear.

Let’s say that we have the following generic class:

class Cup<T>
0*ILzj83W4Ibn4dfjv.jpg

Cup of T

Type parameter T in above declaration does not have any variance modifier (out or in) and by default it is invariant. It means that there is no relation between any two types generated by this generic class. For instance, there is no relation between Cup<Int> and Cup<Number>, Cup<Any> or Cup<Nothing>.

fun main(args: Array<String>) {
val anys: Cup<Any> = Cup<Int>() // Error: Type mismatch
val nothings: Cup<Nothing> = Cup<Int>() // Error: Type mismatch
}

If we need such relation, then we should use variance modifiers: out or in. out makes type parameter covariant. It means that when A is subtype of B and Cup is covariant, then type Cup<A> is subtype of Cup<B>:

class Cup<out T>open class B
class A: B()fun main(args: Array<String>) {
val b: Cup<B> = Cup<A>() // OK
val a: Cup<A> = Cup<B>() // Error: Type mismatch

val anys: Cup<Any> = Cup<Int>() // OK
val nothings: Cup<Nothing> = Cup<Int>() // Error: Type mismatch
}

The opposite effect can be achieved using in modifier, which makes type parameter contravariant. It means that when A is subtype of B and Cup is contravariant, then type Cup<A> is supertype of Cup<B>:

class Cup<in T>open class B
class A: B()fun main(args: Array<String>) {
val b: Cup<B> = Cup<A>() // Error: Type mismatch
val a: Cup<A> = Cup<B>() // OK

val anys: Cup<Any> = Cup<Int>() // Error: Type mismatch
val nothings: Cup<Nothing> = Cup<Int>() // OK
}

You can look at this 3 variance modifiers like at mathematical operations, that are allowing sub-typing when the type is equal, grater-or-equal or smaller-or-equal. But variance modifier meaning is much deeper and I will explain it using big Java problem that comes from the fact that Java arrays are covariant and they allow value setting.

Java problem with the array covariance

In Java, arrays are covariant. Many sources state that the reason behind this decision was to make it possible to create functions, like sort, that makes generic operations on arrays of every type. But there is a big problem with this decision. To understand it, let’s analyze following valid operations, which produce no compilation time error, but instead throws runtime error:

// Java
Integer[] numbers = {1, 4, 2, 1};
Object[] objects = numbers;
objects[2] = "B"; // Runtime error

Result: Runtime exception: Exception in thread “main” java.lang.ArrayStoreException: java.lang.String

As you can see, numbers casting to Object[] didn’t change actual type used inside the structure (it is still Integer), so when we try to assign value of type String to this array, then error occurs. This is really bad! Compiler should prevent such errors in compilation time.

Kotlin is much safer than Java. Arrays in Kotlin have invariant type parameter. List interface has covariant type parameter, because it is immutable. MutableList has invariant type parameter.

Problem with covariance

As you can see, problem with covariance is with mutability after upcasting. If you analyze it more deeply, covariant type parameter — not only on setter, but on any in position (public methods parameters or public property) — is potential source of errors. This is why Kotlin, which is guarding type safeness, prohibits covariant type parameters use on in positions:

class Cup<out T>(
var elem: T // Error: T not allowed on invariant position
) {
fun set(new: T) { // Error: T not allowed on in position
elem = new
} fun get(): T = elem
}

We can correct above code by using type argument only on out positions (public methods return types) and not on in positions (public methods arguments) or invariant positions (public properties):

class Cup<out T>(
private var elem: T
) {
fun get(): T = elem
}

Note, that this is where out variance modifier name comes from: Covariant type parameters are allowed on out positions and they are made using out variance modifier.

Problem with contravariance

Imagine for a moment what would have happened if Java designers decided to make arrays contravariant by default. We wouldn’t have any problems with setting values:

// Java
Number[] nums = {1, 1.0, 1F};
Integer[] ints = nums; // 1
ints[2] = 12;
  1. It is not allowed in Java but would be in our imaginary scenario

Instead, we would have a problem with obtaining values because we would expect Integer even though we can get any type of number:

// Java
Integer i = ints[1]; // Runtime error

Even before this example, you might guess that contravariant parameters, which are made using in modifier in Kotlin, are allowed only on in positions. If you did so, then you are totally right. The reason behind this restriction is that it is the only solution to above problem. Let’s see it in practice:

class Cup<in T>(
var elem: T // Error: T not allowed on invariant position
) {
fun set(new: T) {
elem = new
} fun get(): T = elem // Error: T not allowed on out position
}

We can correct it by using type argument only on in positions (public methods arguments) and not on out positions (public methods return types) or invariant positions (public properties):

class Cup<in T>(
private var elem: T
) {
fun set(new: T) {
elem = new
}
}

Lack of problems with invariance

Note that invariant type argument is allowed in all positions:

class Cup<T>(
var elem: T
) {
fun set(new: T) {
elem = new
} fun get(): T = elem
}

Summary

This all makes Kotlin variance much safer then Java. in and out modifiers might not be intuitive by default, but all you need to remember is this:

  • Default variance behavior of type parameter is invariance. If Cup is invariant and A is subtype of B then there is no relation between Cup<A> and Cup<B>.
  • out makes type parameter covariant. If Cup is covariant and A is subtype of B, then Cup<A> is subtype of Cup<B>. Covariant type can be used on out positions.
  • in makes type parameter contravariant. If Cup is contravariant and A is subtype of B, then Cup<B> is subtype of Cup<A>. Contravariant type can be used on in positions.

To help you learn it, I’ve created Anki deck of flashcards.

We dig deeper, and with practical exercises, into generics in our workshops. Here is the next one:

1*K-f1laplrjQQAMlYKfLgHw.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK