In the below Kotlin code snippet, Producer class is covariant in type parameter T and Consumer class is contravariant in type parameter T. In other words, T is a covariant type parameter of Producer and T is a covariant type parameter of Consumer. So, what do these statements mean?
To understand the meaning of these statements, let’s consider the allowed behaviors. Copy-n-paste the above code snippet at https://try.kotlinlang.org and run it. You will observe two errors — at lines 13 and 15.
class Producer<out T: Any>(val e:T) { | |
fun read(): T = e | |
} | |
class Consumer<in T: Any>() { | |
private lateinit var e: T | |
fun write(v: T): Unit { e = v } | |
} | |
fun main() { | |
var p1: Producer<Number> = Producer<Double>(0.4) | |
p1.read() | |
var p2: Producer<Double> = Producer<Number>(3) // Disallowed | |
p2.read() | |
var c1: Consumer<Number> = Consumer<Double>() // Disallowed | |
c1.write(3) | |
var c2: Consumer<Double> = Consumer<Number>() | |
c2.write(0.4) | |
} |
Covariance
The assignment on line 11 is allowed as Producer<Double>
is treated as a sub-type of Producer<Number>
. This is due to two reasons.
- Values of type
T
can only be retrieved/read fromProducer<T>
instances becauseT
is used to specify only the values provided byProducer
; see the class definition ofProducer
. - Since
Producer<Double>
instance is assigned top1
, all values retrieved/read viap1
will be of typeDouble
, a sub-type of the type argumentNumber
of the declared typeProduce<Number>
ofp1
. So, the behavior exhibited by the instance assigned top1
conforms with (is a subset of) the behavior guaranteed by the declared type ofp1
.
Due to the above sub-typing relation, the assignment on line 13 is not allowed. If it was allowed, p2.read()
would return a value of type Number
when it should return only values of type Double
based on the declared type Producer<Double>
of p2
.
Contravariance
A similar reasoning explains contravariance.
The assignment on line 17 is allowed as Consumer<Number>
is treated as a sub-type of Consumer<Double>
. This is due to two reasons.
- Values of type
T
can only be injected/written intoConsumer<T>
instances becauseT
is used to specify only the values provided toConsumer
; see the class definition ofConsumer
. - Since the type argument
Double
of the declared type ofc2
is a sub-type ofNumber
, all values provided toConsumer<Number>
instance assigned toc2
will be a sub-type of typeNumber
. The behavior allowed by the declared type ofc2
conforms with (is a subset of) the behavior supported by the instance assigned toc2
.
Due to the above sub-typing relation, the assignment on line 15 is not allowed. If it was allowed, c1.write(3)
would store a value of type Number
in the property (field) c1.e
when it should store only values of type Double
in c1.e
based on the Consumer<Double>
instance assigned to c1
.
Summary
In short, if type X
is a sub-type of type Y
and type S<X>
can be treated as sub-type of type S<Y>
, then S
is covariant in T
and T
is a covariant type parameter of the generic type S<T>
.
Similarly, if type X
is a sub-type of type Y
and S<Y>
can be treated as sub-type of type S<X>
, then S
is contravariant in T
and T
is a contravariant type parameter of the generic type S<T>
.
In both definitions, the key is if S<X>
will be treated as sub-type of S<Y>
or vice versa, and this is dependent on how the type parameter T
of S<T>
is used in S
; specifically, in the context of input and output of the operations/methods of S
.
Top comments (0)