DEV Community

Venkatesh-Prasad Ranganath
Venkatesh-Prasad Ranganath

Posted on

Covariance and Contravariance

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.

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 from Producer<T> instances because T is used to specify only the values provided by Producer; see the class definition of Producer.
  • Since Producer<Double> instance is assigned to p1, all values retrieved/read via p1 will be of type Double, a sub-type of the type argument Number of the declared type Produce<Number> of p1. So, the behavior exhibited by the instance assigned to p1 conforms with (is a subset of) the behavior guaranteed by the declared type of p1.

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 into Consumer<T> instances because T is used to specify only the values provided to Consumer; see the class definition of Consumer.
  • Since the type argument Double of the declared type of c2 is a sub-type of Number, all values provided to Consumer<Number> instance assigned to c2 will be a sub-type of type Number. The behavior allowed by the declared type of c2 conforms with (is a subset of) the behavior supported by the instance assigned to c2.

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.

Further Reading

Top comments (0)