The problem in teaching generics with the List examples is that one can understand the examples and still not the rule.
With the advent of generics, the compiler hasn't been instructed with human knowlege, so it doesn't know that a List<A>
is an ordered sequence of elements (it could be a proboscidean with long, curved tusks), and thus it can't guarantee that a List will accept elements of type A
only. All the compiler knows is supplied by the following definition
interface List<E> {
public void add(E);
public E get(int index);
}
This code does not define a single type: it defines an infinite series of types:
interface ListOfStrings {
public void add(String s);
public String get(int index);
}
interface ListOfShapes {
public void add(Shape e);
public Shape get(int index);
}
depending on how you instantiate the parameterized type. Since in Java each subtype is a valid replacement for its supertypes, a Rectangle
is a legal argument to a List<Shape>
's add(Shape s)
.
Here is when one thinks "Generics, I finally got you!". Then he fires an IDE, types
List<Shape> shapes = new LinkedList<Rectangle>();
and the compiler refuses to compile. "- What's going on here? I can add a rect to a list of shapes, but a list of rects is not a list of shapes?". The problem is one thinks in terms of lists, shapes and rectangles, but the compiler doesn't know any of these. It sees
T reference = <expression returning type S>
so it wonders "- Is S
a valid replacement for type T
?". Phrased differently, is T
a parent of S
in the type hierarchy, as constructed according to the Java rules? So it carefully checks its old Java book and finds that no, X<A>
is not a X<B>
parent. Illegal code. Stop.
"- But a list of rectangles definitely contains shapes! I just compiled a program which added a bunch of rectangles to a List<Shape>
!". This doesn't mean anything to a compiler, it's not such a smart program - you see... It can only interpret those few rules it learned some years ago and can't even tell the difference between a List
and a Mammoth
. It only knows its old Java book, and in the book is clearly stated and remarked that there is no subtyping relation between X<A>
and X<B>
, no matter how A
and B
relate to each other. "- If only generics were implemented like arrays... You, stupid Java people..." In fact:
String[] strings = new String[10];
Object[] objects = strings;
objects[0] = new Rectangle(); // ArrayStoreException at runtime
Maybe you now got the point. After all, those Java people are not so stupid... At least, those fancy rules they chosen for their type system serve a purpose, at least they did it for a practical reason. They made arrays covariant, but generics invariant. Since both arrays and lists' contents can be changed (they are mutable), this exposes arrays (but not lists) to the problem seen above. For example, Scala achieves type safety by making lists covariant but immutable (a List[Integer]
can be assigned to a List[Number]
, but they're read-only) and arrays mutable but invariant (you can't use an Array[Integer]
where an Array[Number]
is required, but you can modify their contents).
Finally, to port the subtyping relation between A
and B
into the generics world, one can use the wildcard ?
, but that's matter for a whole new story. I just anticipate that in the old Java book is written that List<Rectangle>
does not extends List<Shape>
, but it's a legitimate child of List<? extends Shape>
.