Search code examples
javagenericstype-inferencediamond-operatorinvariance

Invariant Generics don't seem working correctly


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

Solution

  • 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.