Question

I have read about the generics get and put rule that should prevent you from adding a Banana to a List<? extends Fruit>:

public abstract class Fruit { }
public class Banana extends Fruit { }
public class Apple extends Fruit { }

List<? extends Fruit> lst = new ArrayList<>();
lst.add(new Banana()); // Compile error "The method add(capture#6-of ? extends Fruit) in the type List<capture#6-of ? extends Fruit> is not applicable for the arguments (Banana)"

I understand that if you didn't have the compile error, you would be able to do:

List<? extends Fruit> lst = new ArrayList<>();
lst.add(new Banana());
lst.add(new Apple());

which sounds wrong because you get different objects type in the same list (I saw an answer by @Tom Hawtins explaining why, and I believed him :) (I can't find the link to it though)).

However, if you implement the following:

public class ListFruit implements List<Fruit> {

    List<Fruit> list;

    public ListFruit() {
        list = new ArrayList<>();
    }

    @Override public int size() { return list.size(); }
    // All delegates of "list"
    @Override public List<Fruit> subList(int fromIndex, int toIndex) { return list.subList(fromIndex, toIndex); }
}

and then do:

ListFruit lst = new ListFruit();
lst.add(new Banana());
lst.add(new Apple());
for (Fruit fruit : lst)
    System.out.println(fruit.getClass().getName());

you get no compile error, and the following (expected) output:

Banana
Apple

Am I breaking any contract by doing so? Or am I circumventing a protection and should-no-be-doing-it?

Was it helpful?

Solution

No, it's absolutely fine to have two different kinds of Fruit in a List<Fruit>.

The issue comes when you've actually created a List<Banana>. For example:

List<Banana> bananas = new ArrayList<>();
bananas.add(new Banana());
// This is fine!
List<? extends Fruit> fruits = bananas;
// Calling fruits.get(0) is fine, as it will return a Banana reference, which
// is compatible with a Fruit reference...

// This would *not* be fine
List<Fruit> badFruits = bananas;
badFruits.add(new Apple());
Banana banana = bananas.get(0); // Eek! It's an apple!

OTHER TIPS

Your last code is just fine, but the same code would have worked using just a List<Fruit>. The difference is that a List<? extends Fruit> can be a List whose generic type is any sort of fruit, such as Apple, and you know that calling get() on a List<Apple> will give you an Apple.

It's perfectly acceptable to add an Apple to a List<Fruit>. The difference is that when you pull the object back out of the List, you only know that it's a Fruit, not what specific kind of fruit it is.

Consider the case where your list is a function parameter, not a local variable:

public void eatFruit(List<? extends Fruit> list)
{
    for (Fruit fruit : list)
    {
        eat(fruit);
    }
}

You can pass a List<Banana> or a List<Apple> to this method, or even a List<Cherry>. The compiler knows only that the "?" in "? extends Fruit" represents some class that extends Fruit, but not what that class is. It has no way of knowing whether a Banana is an instance of that class, so it won't let you add one to the list. It will let you read the elements of the list and use them as fruit, as I did.

If you define the list as simply "List<Fruit> list", then it would allow you to add a Banana to the list, or any other type of fruit. But it would not allow you to call the function and pass in a List<Banana>, because a List<Banana> can only contain Bananas, but your function could add any type of fruit.

The whole point of Generics is that if you create a List<Cherry> you should never be able to add a Banana to it (unless you cast it to a raw List, defeating the purpose). The compiler knows that your list contains only fruit of a certain type (possibly any type), but it does not know what type that is. So it won't let you add anything to it. This is the only way to ensure that the list remains pure.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top