Invariant Generics don't seem working correctly

204 views Asked by At

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
3

There are 3 answers

1
Alexander Ivanchenko On BEST ANSWER

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:

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 invariance means - we can assign only List<Number> to a variable of type List<Number> (but we are free to store any subtype of Number in it, it's safe).

As an example, let's consider that 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 expressions (i.e. expressions that appear within a context) that insures that context will always be taken into account by the type inference mechanism.

0
dani-vta On

Covariance is the ability to pass or specify a subtype when a supertype is expected. 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 an instance of B and you're passing new C(), since new C() is an instance of C and also is-a 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. Making a silly example, every thumb is a finger but not every finger is a thumb. A is a generalization of B; therefore, an instance of A cannot appear where an instance of B is expected.

Here there's a good article expanding the concept of covariance in java.

https://www.baeldung.com/java-covariant-return-type

1
andrewJames On

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.