Search code examples
javagenericscollectionsconsumercontravariance

Java Generics PECS , add capture ? super cannot be applied to java.util.list


The issue of variance (particularly contravariance) has got me banging my head against the wall for a week. I have finally understood the theory, thanks to a couple of questions on here, and now as soon as I start working on it, I am getting errors I just don't understand.

I have a simple heirarchy of classes:

abstract class Fruit, Mango extends Fruit, Orange extends Fruit, BloodOrange extends Orange

abstract class Fruit implements PlantEatable {

    private boolean isRipe;

    private boolean isEatable;

    public boolean isRipe() {
        return isRipe;
    }

    public void setRipe(boolean ripe) {
        isRipe = ripe;
    }

    @Override
    public boolean isEatable() {
        return isEatable;
    }

    public void setEatable(boolean eatable) {
        isEatable = eatable;
    }
}

public class Mango extends Fruit {

}

public class Orange extends Fruit{

}

public class BloodOrange extends Orange{

}

Now, The Oracle documentation generally has been great about Generics except for the part that was the most important I find confusing: https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html

If I am doing PECS which is Producer Extends Consumer Super

I am trying to do something very simple:

public class UpperBoundEg {

    public static <E> void copy(List<? extends E> src, List<? super E> dest) {
        src.forEach( item -> {
            dest.add(item);
            System.out.println("Added to dest: " + item.getClass());
        });
    }

    public static <E> void acceptTest(List<? super Orange> objects) {

    }

    public static void main(String[] args) {

        //Producer
        List<? extends Orange> oranges = new ArrayList<>();
        //oranges.add(new Orange()); // Doesn't work because its a producer ?

        //Consumer
        List<? super BloodOrange> objects = new ArrayList<>();
        objects.add(new BloodOrange());
        //objects.add(new Orange()); // Why doesn't it work ?
        //objects.add(new Object()); // Why doesn't it work ?

        copy(
                Arrays.asList(
                        new Orange(),
                        new Orange(),
                        new BloodOrange(),
                        new Object(), 
                        new Mango() // Why is this allowed?
                ),
                new ArrayList<>()
        );

    }
}

Why is this happenning ? I thought List<? super BloodOrange> should take BloodOrange and all its super classes ? Why is it only accepting BloodOrange ?

And Why am I able to add a Mango to the copy function ?

Adding super types to consumer objects, throws error?


Solution

  • List<? extends Orange> oranges = new ArrayList<>();
    oranges.add(new Orange()); // Doesn't work because its a producer ?
    

    You seem to think a List<? extends Orange> oranges means: The list referred to by the variable named oranges can contain 'anything that is either an Orange, or some subtype of it'.

    But that is wrong.

    After all, if that's what you wanted, you would just write List<Orange>. Given:

    BloodOrange a = new BloodOrange();
    Orange b = a; // this is, obviously, legal.
    

    The type of the b variable is Orange, but the object it is pointing at, is actually a BloodOrange. Which is fine. All bloodoranges are oranges. But, therefore, obviously:

    BloodOrange a = new BloodOrange();
    Orange b = a;
    List<Orange> list = new ArrayList<>();
    list.add(b); // this.. has to be legal!
    list.add(a); // it would be ridiculous if this wasn't, then!
    

    The above compiles completely fine. I just added a bloodorange to a List<Orange>. Which, of course, I can do: All BloodOranges are Oranges. So why wouldn't I be able to?

    Hence, for the meaning 'a list that contain anything that is either an Orange or some subtype of it', List<Orange> is it. Not List<? extends Orange>.

    List<? extends Orange> has a different meaning. It means:

    This is a list that is restricted to contain.. I don't actually know. It has some, unknown to this code, restriction. However, what I do is, that the nature of the restriction is either that it can only contain Oranges (and therefore, BloodOranges are also fine), or, some subtype of Orange.

    In other words, this is legal:

    List<BloodOrange> bloodOranges = new ArrayList<BloodOrange>();
    List<? extends Orange> someSortOfOrangesList = bloodOranges;
    

    and now you see why you can't call .add(new Orange()) on a List<? extends Orange>. After all, java is reference based, the above code contains only one new statement so there is just one list, period. You merely have 2 variables that both point at the same list - it's like having an address book with 2 separate pages both listing the same address - that doesn't magically mean there are now somehow 2 houses. So, if you add something to someSortOfOrangesList, you're also adding it to the list pointed at by variable bloodOranges, given that both variables are pointing at the same list. Thus, if you COULD add .add(new Orange()) to a List<? extends Orange>, then I just added a NOT BloodOrange to a list whose type is List<BloodOrange> and I just broke it. Hence why the compiler won't let you, at all.

    Once you understand this, all is clear:

    List<? super BloodOrange> objects = new ArrayList<>();
    objects.add(new BloodOrange());
           //objects.add(new Orange()); // Why doesn't it work ?
           //objects.add(new Object()); // Why doesn't it work ?
    

    Same reason: List<? super BloodOrange> does not mean 'this list can contain bloodoranges or any supertype of it'. Because that's completely pointless - if you want that, just write List<Object>. No, it means: "This List has some unknown to be restriction, however, I do know, that restriction is either BloodOrange, or Orange, or Fruit, or Object. One of those 4, I don't know which one, but.. it has to be one of those 4 or the compiler wouldn't have allowed me to write the assignment".

    And with that specific restriction, you CAN add BloodOrange instances to this list. Because .add(new BloodOrange()) is fine on a List<Orange>, it is also fine on a List<BloodOrange>, and also fine on a List<Fruit>, and also fine on a List<Object>. We know it has to be one of those 4, so it is fine.

    You can't call .add(new Orange()) on this list because that is fine for 3 of the 4 things it could be, but not if your List<? super BloodOrange> variable is pointing at a list whose type is List<BloodOrange>. Because then you'd be adding a not-BloodOrange to it, we don't want that.

    copy(
                   Arrays.asList(
                           new Orange(),
                           new Orange(),
                           new BloodOrange(),
                           new Object(), 
                           new Mango() // Why is this allowed?
                   ),
                   new ArrayList<>()
    

    Because java will figure out that picking Object as a bound works here. Because they're all objects.

    A crucial thing you need to understand here is that class Foo extends Bar means: "Any instance of Foo is just as suitable as a Bar and can be used anytime, anywhere, a Bar is required; the reverse is not necessarily true of course", therefore, they are ALL objects and can ALL be used where an Object would be required. Thus, java goes: Aight, we'll make that E bound to Object, turning it into requiring [A] List<? extends Object> and [B] a List<? super Object> - and List<Object> fits both of those bounds. So, we interpret the List.asList(...) as List.<Object>asList(...) which is fine as every argument to it is an object (all things are objects, so that's obvious then), and we'll interpret new ArrayList<>() as new ArrayList<Object>() and voila that will compile just fine. Hence, that's what happens here.