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?
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!