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 that the list contained only Date
s prior to the assignment. It is also
permissible, albeit 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>
? Can a List<Date>
be 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, 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 additional restriction 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:
List
<
Object
>
lo
;
List
<
Date
>
ld
=
new
ArrayList
<
Date
>();
lo
=
ld
;
// Compile-time Error! Incompatible types.
Although our intuition tells us that the Date
s in that List
could all live happily as Object
s 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 that there is no inheritance
relationship among 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 OK and what’s not OK 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.
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
Object
s 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
;
// Can't really do this
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 previously in relation to Java arrays. Array types have an inheritance relationship that allows this kind of aliasing to occur:
Date
[]
dates
=
new
Date
[
10
];
Object
[]
objects
=
dates
;
objects
[
0
]
=
"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.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.