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
CodePudding user response:
The question's examples don't demonstrate the invariance of generics.
An example which does demonstrate this would be:
ArrayList<Object> ao = new ArrayList<String>(); // does not compile
(You might incorrectly expect the above to compile, because String
is a subclass of Object
.)
The question shows us different ways to construct Container<B>
objects - some of which compile and others which do not, because of the inheritance hierarchy of A
, B
and C
.
That diamond operator <>
means that the created container is of type B
in every case.
If you take the following example:
Container<B> contc = new Container<>(new C()); // compiles
And re-write it by populating the diamond with C
, the you will see that the following does not compile:
Container<B> contc = new Container<C>(new C()); // does not compile
That will give you the same "incompatible types" compilation error as my ArrayList
example.
CodePudding user response:
Covariance is the ability to pass or specify a subtype when a supertype is expetced. If your C class extends B, then C is a child class of B. This relationship between C and B is also called is-a
relationship, where an instance of C is also an instance of B. Therefore when your variable contc
is expecting a B instance and you're passing new C()
, since new C()
is an instance of C and C instance is (also)-an
instance of B, then the compiler allows the following writing:
Container<B> contc = new Container<>(new C());
Conversely, when you're writing
Container<B> conta = new Container<>(new A());
you're receiving an error because A is a supertype of B, there is no is-a
relationship from A to B, but rather from B to A. This is because every instance of B is also an instance of A, but not every instance of A is an instance of B (To make a silly example, every thumb is a finger but not every finger is a thumb). A is a generalization of B; therefore it cannot appear where a B instance is expected.
Here there's a good article expanding the concept of covariance in java.
https://www.baeldung.com/java-covariant-return-type
CodePudding user response:
One of the boons of Java 7 introduced is 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 instantiation of 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:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
the compiler will infer the type of the ArrayList
instance from the type of the variable people
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:
A class instance creation expression is a poly expression (§15.2) if it uses the diamond form for type arguments to the class, and it appears in an assignment context or an invocation context (§5.2, §5.3).
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 type B
.
Container<B> conta = new Container<>(new A()); // 1 - ERROR because `B t = new A()` is incorrect
Container<B> contb = new Container<>(new B()); // 2 - fine because `B t = new B()` is correct
Container<B> contc = new Container<>(new C()); // 3 - fine because `B t = new C()` is also correct
Since all instances of container are of type B
and of parameter type expected by the contractor also will be B
. I.e. can provide an instance of B
or any of its subtypes. Therefore, in the case 1
we are getting a compilation error, meanwhile 2
and 3
(B
and subtype of B
) 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 of Integer
, Byte
, Double
, etc., that would not lead to any problem since they all can represent their super type Number
. But the compiler will not allow assigning this list to any list that is not of type List<Number>
because otherwise it would be impossible to ensure that this assignment is safe. And that is what the covariance means.
As an example, let's consider there's a setter method in the Container
class:
public class Container<T> {
public T t;
public Container(T t) {
this.t = t;
}
public void setT(T t) {
this.t = t;
}
}
Now let's use it:
Container<B> contb = new Container<>(null); // to avoid any confusion initialy `t` will be assigned to `null`
contb.setT(new A()); // compilation error - because expected type is `B` or it's subtype
contb.setT(new B()); // fine
contb.setT(new C()); // fine because C is a subtype of B
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()
expects Container<B>
, all instances above will be inferred as being of type B
.
method(new Container<>(new A())); // Error
method(new Container<>(new B())); // OK - because `B t = new B()` is correct
method(new Container<>(new C())); // OK - because `B t = new C()` is also correct
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 of Container<C>
. It means your initial guess was somewhat correct: with Java 7 the below statement would not compile.
Container<B> contc = new Container<>(new C()); // Container<B> = Container<C> - is an illegal assignment
But Java 8 has introduced a feature called target types and poly expression (i.e. expressions that appear within a context) that insures that context will always be taken into account by the type inference mechanism.