Generics and Relationships in Java - Parameterized Type Relationships (Page 2 of 5 )
We know now that parameterized types share a common, raw type. This is why our parameterized List<Date> is just a List at runtime. In fact, we can assign any instantiation of List to the raw type if we want:
List list = new ArrayList<Date>();
We can even go the other way and assign a raw type to a specific instantiation of the generic type:
List<Date> dates = new ArrayList(); // unchecked warning
This statement generates an unchecked warning on the assignment, but thereafter the compiler trusts you that the list contained only Dates prior to the assignment. It is also permissible, but pointless, to perform a cast in this statement. We'll talk about casting to generic types a bit later.
Whatever the runtime types, the compiler is running the show and does not let us assign things that are clearly incompatible:
List<Date> dates = new ArrayList<String>(); // Compile-time Error!
Of course, the ArrayList<String> does not implement the methods of List<Date> conjured by the compiler, so these types are incompatible.
But what about more interesting type relationships? The List interface, for example, is a subtype of the more general Collection interface. Is a particular instantiation of the generic List also assignable to some instantiation of the generic Collection? Does it depend on the type parameters and their relationships? Clearly, a List<Date> is not a Collection<String>. But is a List<Date> a Collection<Date>? What about a List<Date> being a Collection<Object>?
We'll just blurt out the answer first, then walk through it and explain. The rule is that for the simple types of generic instantiations we've discussed so far in this chapter inheritance applies only to the "base" generic type and not to the parameter types. Furthermore, assignability applies only when the two generic types are instantiated on exactly the same parameter type. In other words, there is still one-dimensional inheritance, following the base generic class type, but with the catch that the parameter types must be identical.
For example, recalling that a List is a type of Collection, we can assign instantiations of List to instantiations of Collection when the type parameter is exactly the same:
Collection<Date> cd; List<Date> ld = new ArrayList<Date>(); cd = ld; // Ok!
This code snippet says that a List<Date> is a Collection<Date>--pretty intuitive. But trying the same logic on a variation in the parameter types fails:
This code snippet says that a is a --pretty intuitive. But trying the same logic on a variation in the parameter types fails:
List<Object> lo; List<Date> ld = new ArrayList<Date>(); lo = ld; // Compile-time Error! Incompatible types.
Although our intuition tells us that the Dates in that List could all live happily as Objects in a List, the assignment is an error. We'll explain precisely why in the next section, but for now just note that the type parameters are not exactly the same and, as we've said, there is no inheritance relationship among the parameter types in generics. This is a case where thinking of the instantiation in terms of types and not in terms of what they do helps. These are not really a "list of dates" and a "list of objects" but more like a DateList and an ObjectList, the relationship of which is not immediately obvious.
Try to pick out what's okay and what's not okay in the following example:
Collection<Number> cn; List<Integer> li = new ArrayList<Integer>(); cn = li; // Compile-time Error! Incompatible types.
It is possible for an instantiation of List to be an instantiation of Collection, but only if the parameter types are exactly the same. Inheritance doesn't follow the parameter types and this example fails.
One more thing: earlier we mentioned that this rule applies to the simple types of instantiations we've discussed so far in this chapter. What other types are there? Well, the kinds of instantiations we've seen so far where we plug in an actual Java type as a parameter are called concrete type instantiations. Later we'll talk about wildcard instantiations, which are akin to mathematical set operations on types. We'll see that it's possible to make more exotic instantiations of generics where the type relationships are actually two-dimensional, depending both on the base type and the parameterization. But don't worry, this doesn't come up very often and is not as scary as it sounds.
Why Isn't a List<Date> a List<Object>?
It's a reasonable question. Even with our brains thinking of arbitrary DateList and ObjectList types, we can still ask why they couldn't be assignable. Why shouldn't we be able to assign our List<Date> to a List<Object> and work with the Date elements as Object types?
The reason gets back to the heart of the rationale for generics that we discussed in the introduction: changing APIs. In the simplest case, supposing an ObjectList type extends a DateList type, the DateList would have all of the methods of ObjectList and we could still insert Objects into it. Now, you might object that generics let us change the APIs, so that doesn't apply anymore. That's true, but there is a bigger problem. If we could assign our DateList to an ObjectList variable, we would have to be able to use Object methods to insert elements of types other than Date into it. We could alias the DateList as an ObjectList and try to trick it into accepting some other type:
DateList dateList = new DateList(); ObjectList objectList = dateList; objectList.add( new Foo() ); // should be runtime error!
We'd expect to get a runtime error when the actual DateList implementation was presented with the wrong type of object. And therein lies the problem. Java generics have no runtime representation. Even if this functionality were useful, there is no way with the current scheme for Java to know what to do at runtime. Another way to look at it is that this feature is simply dangerous because it allows for an error at runtime that couldn't be caught at compile time. In general, we'd like to catch type errors at compile time. By disallowing these assignments, Java can guarantee that your code is typesafe if it compiles with no unchecked warnings.
Actually, that last sentence is not entirely true, but it doesn't have to do with generics; it has to do with arrays. If this all sounds familiar to you, it's because we mentioned it already in relation to Java arrays. Array types have an inheritance relationship that allows this kind of aliasing to occur:
Date  dates = new Date; Object  objects = dates; objects = "not a date"; // Runtime ArrayStoreException!
However, arrays have runtime representations as different classes and they check themselves at runtime, throwing an ArrayStoreException in just this case. So, in theory, Java code is not guaranteed typesafe by the compiler if you use arrays in this way.