javagenericsgeneric-listpecs

Overcoming generics put-get rule


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?


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!