I've read some articles about Covariance, Contravariance, and Invariance in Java, but I'm confused about them.
I'm using Java 11, and I have a class hierarchy A => B => C
(means that C
is a subtype of B
and A
, and B
is a subtype of A
) and a class Container
:
class Container<T> {
public final T t;
public Container(T t) {
this.t = t;
}
}
for example, if I define a function:
public Container<B> method(Container<B> param){
...
}
here is my confusion, why does the third line compile?
method(new Container<>(new A())); // ERROR
method(new Container<>(new B())); // OK
method(new Container<>(new C())); // OK Why ?, I make a correction, this compiles OK
if in Java Generics are invariant.
When I define something like this:
Container<B> conta = new Container<>(new A()); // ERROR, Its OK!
Container<B> contb = new Container<>(new B()); // OK, Its OK!
Container<B> contc = new Container<>(new C()); // Ok, why ? It's not valid, because they are invariant
One of the boons introduced with Java 7 is the so-called diamond operator
<>
.And it has been with us for so long, that it's easy to forget that every time when diamond is being used while instantiating a generic class the compiler should infer the generic type from the context.
If we define a variable which will hold a reference to a list of
Person
objects like this:the compiler will infer the type of the
ArrayList
instance from the type of the variablepeople
on the left.In the Java language specification, the expression
new ArrayList<>()
is being described as a class instance creation expression and because it doesn't specify the generic type parameter and is used within a context, it should be classified as being a poly expression. A quote from the specification:I.e. when diamond
<>
is used with a generic class instantiation, the actual type will depend on the context in which it appears.The three statements below represent the case of so-called assignment context. And all three instances
Container
will be inferred as being of typeB
.Since all instances of container are of type
B
and of parameter type expected by the contractor also will beB
. I.e. can provide an instance ofB
or any of its subtypes. Therefore, in the case1
we are getting a compilation error, meanwhile2
and3
(B
and subtype ofB
) will compile correctly.And it in't a violation of invariant behavior. Think about it this way: we can store in a
List<Number>
instances ofInteger
,Byte
,Double
, etc., that would not lead to any problem since they all can represent their super typeNumber
. But the compiler will not allow assigning this list to any list that is not of typeList<Number>
because otherwise it would be impossible to ensure that this assignment is safe. And that is what the invariance means - we can assign onlyList<Number>
to a variable of typeList<Number>
(but we are free to store any subtype ofNumber
in it, it's safe).As an example, let's consider that there's a setter method in the
Container
class:Now let's use it:
When we deal with a class instance creation expression using diamond
<>
, which is passed to a method as an argument, the type will be inferred from the invocation context as the quote from the specification provided above states.Because
method()
expectsContainer<B>
, all instances above will be inferred as being of typeB
.Note
The important thing to mention that prior to Java 8 (i.e. with Java 7, because we are using diamond) the expression
new Container<>(new C())
will be interpreted by the compiler as a standalone expression (i.e. the context will be ignored) creating an instance ofContainer<C>
. It means your initial guess was somewhat correct: with Java 7 the below statement would not compile.But Java 8 has introduced a feature called target types and poly expressions (i.e. expressions that appear within a context) that insures that context will always be taken into account by the type inference mechanism.