1. Use site variance
Sample: list with contravariance acts as a consumer.
It is safe to add into list the object of declared type B, or it's subtypes. It is not safe to retrieve object of type B from the list, because list of objects with super types of B may be provided. However, it is safe to retrieve the Kotlin most basic type Any? from the list.
open class A open class B : A() class C : B() // Can receive list of B or its super types (A, Any, Any? types in this sample). // Allows to put B or B subtypes into list (C in this sample). // Cannot get fom List - except, can only get most generic type Any? from list. fun listAsConsumer(list: MutableList<in B>) { // Type mismatch: inferred type is Any? but B was expected // val b: B = list[0] // Type mismatch: inferred type is A but B was expected // list.add(A()) // Cannot get fom List - except, can only get most generic type Any? from list. val any: Any? = list[0] // Allowed to add B or its subtypes into list list.add(B()) list.add(C()) } fun main() { listAsConsumer(mutableListOf<A>(A())) }
Sample: contravariance in Comparator.
Compare list of Ints using Comparator<Number>.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<in T>): List<T> { return this.sortedWith(comparator) } fun main() { val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number -> o1.toDouble().compareTo(o2.toDouble()) } val intList: List<Int> = listOf(4, 7) val result: List<Int> = intList.sortedWithWrapper(comparator) }
The same sample without contravariance for Comparator is below. Without contravariance sortedWithWrapper() function could not return List<Int>, but only List<Number>. This happens because List<Int> is upcasted to List<Number>, to match the invariant T of the Comparator.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<T>): List<T> { return this.sortedWith(comparator) } fun main() { val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number -> o1.toDouble().compareTo(o2.toDouble()) } val intList: List<Int> = listOf(4, 7) // Type mismatch: inferred type is List<Number> but List<Int> was expected // val result: List<Int> = intList.sortedWithWrapper(comparator) val result: List<Number> = intList.sortedWithWrapper(comparator) }
2. Declaration site variance
Declaration site variance is available in Kotlin, unlike Java where only use-site variance is possible.
Sample: Class with contravariance. Consumer cannot have contravariant type T in invariant or out positions.
open class A open class B : A() class C : B() class Consumer<in T> { // Type parameter T is declared as 'in' but occurs in 'invariant' // position in type T? // var x: T? = null fun consume(x: T) {} // Ok, List is defined as List<out T>, // so it is a producer for this Consumer class fun consume(x: List<T>) {} // Type parameter T is declared as 'in' but occurs in 'invariant' position // in type Array<T> // fun consume(x: Array<T>) {} // Type parameter T is declared as 'in' but occurs in 'out' position // in type T? // fun produce() : T? { return x } } fun main() { // Consumer of B (can be assigned Consumer of supertypes (A, Any, Any?)), // but now minimum B or its subtypes are accepted into consume() val consumerB: Consumer<B> = Consumer<A>() consumerB.consume(C()) // Type mismatch: inferred type is A but B was expected // consumerC.consume(A()) // Type mismatch: inferred type is Consumer<B> but Consumer<A> was expected // val consumerA: Consumer<A> = Consumer<B>() }
Consumer class with contravariant T cannot accept another consumer with contravariant T.
class AnotherConsumer<in T> { fun consume() {} } class Consumer<in T> { // Type parameter T is declared as 'in' but occurs in 'out' position // in type AnotherConsumer// fun consume(x: AnotherConsumer<T>) {} }
This is due to AnotherConsumer can be extended by the class that cancels the contravariant 'in' modifier:
open class AnotherConsumer<in T> { open fun consume(x: T) {} } class AnotherConsumerChild: AnotherConsumer<T>() { var x: T? = null override fun consume(x: T) { this.x = x } fun getT(): T? { return x } }
Allowing to accept another contravariant consumer would lead to ClassCastException.
As an example of the above, in this snippet AnotherConsumerC would be called with the wrong type A (omitting compiler variance checking with @UnsafeVariance annotation).
open class A open class B : A() class C : B() { fun c() {} } open class AnotherConsumer<in T> { open fun consume(x: T) {} } // Overrides contravariance in parent type class AnotherConsumerC: AnotherConsumer<C>() { override fun consume(x: C) { x.c() } } open class Consumer<in T> { open fun consume(anotherConsumer: AnotherConsumer<@UnsafeVariance T>) {} } open class ConsumerA: Consumer<A>() { override fun consume(anotherConsumer: AnotherConsumer<A>) { anotherConsumer.consume(A()) } } fun main() { val consumerA = ConsumerA() // Allowed because parent Consumer<in T> is contravariant val consumerC: Consumer<C> = consumerA // Produces Exception in thread "main" // java.lang.ClassCastException: A cannot be cast to C consumerC.consume(AnotherConsumerC()) }