-1

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?

  • 2
    Because a `List super BloodOrange>` may actually be a `List`, and so it's an error to add anything to it that is not a `BloodOrange` (i.e., an instance of the class itself or a subclass). – Slaw Mar 07 '23 at 21:36
  • 1
    It may make more sense if you had `void populate(List super BloodOrange> list) { ... }` and then called `populate` with an instance of `List`. The call to the method will succeed, and that setup should help you see why adding anything other than `BloodOrange` to `list` is an error. – Slaw Mar 07 '23 at 21:38
  • Okay, Im just confused because what I read was super T> means that type and any super class of that, so I just thought it should be accepted. Aah!!! Maybe i just had a light bulb moment, does this mean a single collection can only have one type ? When we say List super T> someList, does that mean someList can be a list of T or a super type of T, but not the same list which has all the super types ?? I am really sorry about the silly question –  Mar 07 '23 at 21:45
  • Does this mean say if I have List super BloodOrange> that means I can pass to it a List, List, List ? But I can't create a single List of type super BloodOrange> with all kinds of BloodOranges, Oranges and Objects in the same list ? This would answer everything –  Mar 07 '23 at 21:50
  • 1
    I don't think this is an exact duplicate, but you may find some helpful information among the answers to [this question](https://stackoverflow.com/q/2745265) – Dawood ibn Kareem Mar 07 '23 at 22:41

1 Answers1

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

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Thank you so much for this, I can't believe it took so long to get it. So basically Does this mean say if I have List super BloodOrange> that means I can pass to it a List, List, List ? But I can't create a single List of type super BloodOrange> with all kinds of BloodOranges, Oranges and Objects in the same list ? And the same would apply in reverse if it was extends Orange> ? –  Mar 07 '23 at 21:58
  • Very well put. This should be a canonical answer for questions of this type. – VGR Mar 07 '23 at 23:12
  • 2
    _ that means I can pass to it a List, List, List ?_ - no - generics are invariant. If a method requires a `List`, then only a `List` will do. Or an `ArrayList`, but no other list. Generally it's method params that use `? extends` or `? super` to allow callers to be more flexible. You don't ever write `new ArrayList super BloodOrange>()` - that is useless, the super/extends stuff is for types of params in methods. – rzwitserloot Mar 07 '23 at 23:36
  • PECS says: Producer extends, Consumer super. If you user neither super nor extends, then you're saying: This method both consumes _and_ produces (and therefore only this exact thing will do). A method that takes a `List` both has the intent to add oranges to the list _and_ read objects from it, and treat them as oranges. (Which is why only `List` will do. It's the only one that can do both). – rzwitserloot Mar 08 '23 at 10:55
  • 1
    Just one addendum. Immutable lists can’t subvert the type safety, as their `add` methods will throw an exception unconditionally. Therefore, they allow type transitions not covered by the rules above. E.g. `List bloodOranges = new ArrayList(); List orangesListView = Collections.unmodifiableList(bloodOranges);` So when you have a `List super BloodOrange>` its actual element type might be one of the four mentioned types (five, actually, `PlantEatable` is possible too) *or* it might be an immutable list in which case the actual element type becomes meaningless. – Holger Mar 13 '23 at 12:54