Search code examples
javaoopgenericsextendsgeneric-programming

Why cannot pass the class extended from another generic class as generic class?


I develop some big project. Some parts of my code:

1)

private final ObjectPool<ProcessorMechanicsRoom> processorsPool;
... new ProcessorMechanicsRoom(processorsPool);
public class ProcessorMechanicsRoom
        extends ProcessorMechanics<ProcessMechanicsRoom, IMechanicsRoom, IMechanicsRoomCallback> {
    ...
    public ProcessorMechanicsRoom(ObjectPool<ProcessorMechanicsRoom> pool) {
}
    super(pool); // the problem is here
}

public class ProcessorMechanics
        <P extends ProcessMechanics<M,C>, M extends IAMechanics<C>, C extends IAMechanicsCallback>
        extends Processor<P> {
    private final ObjectPool<ProcessorMechanics<P,M,C>> pool;
    ...
    public ProcessorMechanics(ObjectPool<ProcessorMechanics<P,M,C>> pool) {...}
    ...

}

The problem is that ObjectPool<ProcessorMechanicsRoom> cannot be passed into super-constructor (code 2). So i am confused.


Solution

  • There's this thing called variance.

    Let's use some types we are all familiar with:

    java.lang.Integer extends java.lang.Number extends java.lang.Object

    Covariance

    In a covariant system, you can write:

    Number x = new Integer();

    but you cannot write:

    Integer y = new Number();

    As you might surmise, basic assignment and such in java is all covariant. But that's not the only way to do it.

    Contravariance

    In a contravariant system, you cannot write:

    Number x = new Integer();

    but on the flipside, this actually works:

    Integer y = new Number();

    Invariance

    This is the inflexible one; in this one, neither works. The only thing you can do is:

    Integer y = new Integer();

    Okay, so, what about generics?

    Whereas java is covariant for basic stuff, generics isn't. Generics is contravariant, or covariant, or invariant, depending on how you write the generics.

    • Covariant: List<? extends Number> list = new ArrayList<Integer>(); // legal
    • Contravariant: List<? super Integer> list = new ArrayList<Number>(); // legal
    • Invariant: List<Integer> list = new ArrayList<Integer>(); // only integer will do here

    You've picked invariant. So only ProcessorMechanics will do; your ProcessorMechanicsRoom is a subclass, and therefore you can't do that unless your type relationship allows covariance, and it does not. Make that ? extends and it'll work.

    Um, wtf? Why???

    Because... life. That is how real life works.

    Imagine it did not. I can do this, then, and break everything:

    List<Integer> ints = new ArrayList<Integer>();
    List<Number> numbers = ints; // MARK THIS LINE!
    numbers.add(new Double(5.0));
    Integer x = ints.get(0); // ERROR!
    

    In the above, if it had compiled and run, the last line would be an error, as the .get(0) call would retrieve a double value which isn't an integer. Fortunately, the above does not compile; the error occurs on the marked line. That's.. because the compiler should disallow this. Generics by its very nature are invariant.

    Now, covariance can exist. For example, if you have a method that will sum up the result of invoking .intValue() on each of the Numbers inside, then you could write:

    public int sumAll(List<Number> list) {
       int result = 0;
       for (Number n : list) result += n.intValue();
       return result;
    }
    

    but that's a bad way to write it; you've decreed that the parameter is invariant, thus, you cannot pass a List<Integer> to this thing. But the code is covariant. It would work just as well if you pass a list of integers. So, you should write that as public int sumAll(List<? extends Number> numbers) instead.

    Here is an example of invariance:

    public void addSumToEnd(List<Number> list) {
        int sum = 0;
        for (Number n : list) sum += n.intValue();
        list.add(sum);
    }
    

    Because we're adding a number here, you couldn't write List<? extends Number>. After all, we're adding an int and you can't do that to a List<Double>. The only acceptable lists you can feed in here are List<Number> and List<Integer> and there's no way to express that in java.

    For lists, it's easy: "contravariance = adds" (.add(), .addAll(), etc), "covariance = reads", "invariance = does both". For other generified types it may not be that simple.

    Presumably if your ProcessorMechanics class will only ever 'read', then you can make it covariant, and write:

    public ProcessorMechanics(ObjectPool<? extends ProcessorMechanics<P, M, C>> pool) {...}