Search code examples
javagenericscovariancecontravariance

Java covarianc/contravariance with add/get


I am trying to digest a standard example of using a covariant/contravariant type argument for a Collection and then trying to work out why certain methods behave in a way they do. Here is my example and my (potentially wrong) explanation, and in case of confusion, questions:

List<? extends MyObject> l = new ArrayList<>();
x = l.get(0);

type of x is MyObject -> this is because compiler realizes that that the upper bound on the type is MyObject and can safely assume this type.

l.add(new MyObject()); //compile error

The error is caused because, although the upper-bound type of the collection is known, the ACTUAL type is not known (all the compiler knows is that it is the subclass of MyObject). So, we could have a List<MyObject>, List<MyObjectSubclass> or god knows what other subtype of MyObject as type parameter. As a result, in order to be sure that objects of wrong type are not stored in the Collection, the only valid value is null, which is a subtype of all types.

Conversly:

List<? super MyObject> l = new ArrayList<>();
x = l.get(0)

type of x is Object, as compiler only knows about the lower-bound. Therefore,. teh only safe assumption is the root of type hierarchy.

l.add(new MyObject()); //works
l.add(new MyObjectSubclass()); // works
l.add(new Object()); //fails

The above final case is where I am having problems and I am not sure whether I get this right. COmpiler can expect any list with a generic type of MyObject all the way to the Object. So, adding MyObjectSubclass is safe, because it would be OK to add to List<Object> even. The addition of Object, would however, violate List<MyObject>. Is that more or less correct? I would be glad to hear more technincal explanation if anyone has it


Solution

  • Generics are neither covariant nor contravariant. They are invariant.

    Wildcards can be used to facilitate usage of parameterized types. About 95% of what you need to know about them has been summed up by Mr. Bloch in Effective Java (must read) with the PECS rule (Producer Extends Consumer Super).

    Assume

    interface Owner<T> {
    
        T get();
    
        void set(T t);
    }
    

    and the usual Dog extends Animal example

    Owner<? extends Animal> o1;
    Animal a = o1.get(); //legal
    Dog d = (Dog) o1.get(); //unsafe but legal
    o1.set(new Dog()); //illegal
    

    Conversely:

    Owner<? super Animal> o1;
    o1.set(new Dog()); //legal
    Animal a = o1.get(); //illegal
    

    To answer more directly List<? super Dog> is a list that consumes (adds in this case) Dog instances (meaning it will consume a Poodle instance as well).

    More generally, the methods of a Foo<? super Bar>instance that are defined to accept an argument of Foo's type parameter, can be called with any object referenced compile time with Bar or a sub-type of Bar.