Variance¶
Note: This is an advanced section. Feel free to skip it initially.
Variance deals with the question, which generic types are compatible with each other. We explain this concept using the following class:
The class is called Stack
and has a single type parameter, which is supposed to the denote the type of the elements of the stack. With its [constructor], we can specify the initial elements of the stack. Moreover, two methods are defined on the stack:
push
is supposed to add a new element to the top of the stack.pop
is supposed to remove and return the topmost element of the stack.
We will now try to answer the following two questions:
- If
A
is a subclass ofB
, can we assignStack<A>
toStack<B>
? - If
B
is a subclass ofA
, can we assignStack<A>
toStack<B>
?
Invariance¶
By default, the answer to both questions is "no". The reason for this is that it can allow illegal behavior:
Say, we expect a Stack<Number>
, but pass a Stack<Int>
(Int
is a subclass of Number
). If we can treat the Stack<Int>
as a Stack<Number>
, we are also allowed to add values of type Float
to it. This would also change the original Stack<Int>
, which now contains illegal floating point values.
Now, imagine we a Stack<Number>
, but pass a Stack<Any>
(Number
is a subclass of Any
). If we can treat the Stack<Any>
as a Stack<Number>
, we could read values from the stack that are not numbers, for example strings, since the original stack can contain Any
value.
To sum this up, we cannot assign Stack<A>
to Stack<B>
if A
is a subclass of B
because we might write to the Stack<B>
and alter the original Stack<A>
in an illegal way. Likewise, we cannot assign Stack<A>
to Stack<B>
if B
is a subclass of A
because we might read something from the Stack<B>
that is not of type B
.
We say, the type parameter T
of the class is invariant. It must be matched exactly. The conditions we describe above, however, already give us the information under which circumstances we can loosen this requirement.
Covariance¶
We now want a Stack<A>
to be assignable to a Stack<B>
if A
is a subclass of B
. This behavior is called covariance since the type compatibility relation between Stack<A>
and Stack<B>
points in the same direction as the type compatibility relation between A
and B
.
As outlined above, we can only allow covariance if we forbid writing access. This means that a type parameter that is covariant can only be used for reading. Concretely, a covariant type parameter can only be used as the type of a result not the type of a parameter. We also say the type parameter can only be used in the out-position (i.e. as output), which motivates the keyword out
to denote covariance (see Section Specifying Variance).
In the Stack
example, we can make the class covariant by adding the keyword out
to the type parameter T
and removing the writing method push
:
Contravariance¶
We now want a Stack<A>
to be assignable to a Stack<B>
if B
is a subclass of A
. This behavior is called contravariance since the type compatibility relation between Stack<A>
and Stack<B>
points in the opposite direction as the type compatibility relation between A
and B
.
As outlined above, we can only allow contravariance if we forbid reading access. This means that a type parameter that is contravariant can only be used for writing. Concretely, a contravariant type parameter can only be used as the type of a parameter not the type of a result. We also say the type parameter can only be used in the in-position (i.e. as input), which motivates the keyword in
to denote contravariance (see Section Specifying Variance).
In the Stack
example, we can make the class contravariant by adding the keyword in
to the type parameter T
and removing the reading method pop
:
Specifying Variance¶
The variance of a type parameter can only be declared at its declaration site, using the syntax shown in the following table:
Desired Variance | Declaration Site |
---|---|
Invariant | class Stack<T> |
Covariant | class Stack<out T> |
Contravariant | class Stack<in T> |