In the movie The Matrix,[22] the hero Neo is offered a choice. Take the blue pill and remain in the world of fantasy, or take the red pill and see things as they really are. In dealing with generics in Java, we are faced with a similar ontological dilemma. We can go only so far in any discussion of generics before we are forced to confront the reality of how they are implemented. Our fantasy world is one created by the compiler to make our lives writing code easier to accept. Our reality (though not quite the dystopian nightmare in the movie) is a harsher place, filled with unseen dangers and questions. Why don’t casts and tests work properly with generics? Why can’t I implement what appear to be two different generic interfaces in one class? Why is it that I can declare an array of generic types, even though there is no way in Java to create such an array?!? We’ll answer these questions and more in this chapter, and you won’t even have to wait for the sequel. Let’s get started.
The design goals for Java generics were formidable: add a radical new syntax to the language that safely introduces parameterized types with no impact on performance and, oh, by the way, make it backward-compatible with all existing Java code and don’t change the compiled classes in any serious way. It’s actually quite amazing that these conditions could be satisfied at all and no surprise that it took a while. But as always, compromises were required, which lead to some headaches.
To accomplish this feat, Java employs a technique called erasure, which relates to the idea that since most everything we do with generics applies statically at compile time, generic information does not need to be carried over into the compiled classes. The generic nature of the classes, enforced by the compiler can be “erased” in the compiled classes, which allows us to maintain compatibility with nongeneric code. While Java does retain information about the generic features of classes in the compiled form, this information is used mainly by the compiler. The Java runtime does not know anything about generics at all.
Let’s take a look at a compiled generic class: our friend,
List
. We can do this easily with the
javap command:
%
javap
java
.
util
.
List
public
interface
java
.
util
.
List
extends
java
.
util
.
Collection
{
...
public
abstract
boolean
add
(
java
.
lang
.
Object
);
public
abstract
java
.
lang
.
Object
get
(
int
);
The result looks exactly like it did prior to Java generics, as
you can confirm with any older version of the JDK. Notably, the type of
elements used with the add()
and
get()
methods is Object
. Now, you might think that this is just
a ruse and that when the actual type is instantiated, Java will create a
new version of the class internally. But that’s not the case. This is
the one and only List
class, and it
is the actual runtime type used by all parameterizations of List
; for example, List<Date>
and List<String>
, as we can confirm:
List
<
Date
>
dateList
=
new
ArrayList
<
Date
>();
System
.
out
.
println
(
dateList
instanceof
List
);
// true!
But our generic dateList
clearly does not implement the List
methods just discussed:
dateList
.
add
(
new
Object
()
);
// Compile-time Error!
This illustrates the somewhat schizophrenic nature of Java
generics. The compiler believes in them, but the runtime says they are
an illusion. What if we try something a little more sane and simply
check that our dateList
is a List<Date>
:
System
.
out
.
println
(
dateList
instanceof
List
<
Date
>
);
// Compile-time Error!
// Illegal, generic type for instanceof
This time the compiler simply puts its foot down and says, “No.”
You can’t test for a generic type in an instanceof
operation. Since there are no
actual differentiable classes for different parameterizations of
List
at runtime, there is no way for
the instanceof
operator to tell the
difference between one incarnation of List
and another. All of the generic safety
checking was done at compile time and now we’re just dealing with a
single actual List
type.
What has really happened is that the compiler has erased all of
the angle bracket syntax and replaced the type variables in our List
class with a type that can work at
runtime with any allowed type: in this case, Object
. We would seem to be back where we
started, except that the compiler still has the knowledge to enforce our
usage of the generics in the code at compile time and can, therefore,
handle the cast for us. If you decompile a class using a List<Date>
(the
javap command with the -c option shows you the bytecode, if
you dare), you will see that the compiled code actually contains the
cast to Date
, even though we didn’t
write it ourselves.
We can now answer one of the questions we posed at the beginning
of the section (“Why can’t I implement what appear to be two different
generic interfaces in one class?”). We can’t have a class that
implements two different generic List
instantiations because they are really the same type at runtime and
there is no way to tell them apart:
public
abstract
class
DualList
implements
List
<
String
>,
List
<
Date
>
{
}
// Error: java.util.List cannot be inherited with different arguments:
// <java.lang.String> and <java.util.Date>
Although the compiler treats different parameterizations
of a generic type as different types (with different APIs) at compile
time, we have seen that only one real type exists at runtime. For
example, the class of List<Date>
and List<String>
share the plain old Java
class List
. List
is called the raw
type of the generic class. Every generic has a raw type. It
is the degenerate, “plain” Java form from which all of the generic type
information has been removed and the type variables replaced by a
general Java type like Object
.[23]
It is still possible to use raw types in Java just as before generics were added to the language. The only difference is that the Java compiler generates a warning wherever they are used in an “unsafe” way. For example:
// nongeneric Java code using the raw type
List
list
=
new
ArrayList
();
// assignment ok
list
.
add
(
"foo"
);
// Compiler warning on usage of raw type
This snippet uses the raw List
type just as old-fashioned Java code prior to Java 5 would have. The
difference is that now the Java compiler issues an unchecked
warning about the code if we attempt to insert an object into
the list.
%
javac
MyClass
.
java
Note:
MyClass
.
java
uses
unchecked
or
unsafe
operations
.
Note:
Recompile
with
-
Xlint:
unchecked
for
details
.
The compiler instructs us to use the -Xlint:unchecked
option
to get more specific information about the locations of unsafe
operations:
%
javac
-
Xlint:
unchecked
MyClass
.
java
warning:
[
unchecked
]
unchecked
call
to
add
(
E
)
as
a
member
of
the
raw
type
java
.
util
.
List:
list
.
add
(
"foo"
);
Note that creating and assigning the raw ArrayList
does not generate a warning. It is
only when we try to use an “unsafe” method (one that refers to a type
variable) that we get the warning. This means that it’s still OK to use
older-style, nongeneric Java APIs that work with raw types. We only get
warnings when we do something unsafe in our own code.
One more thing about erasure before we move on. In the previous
examples, the type variables were replaced by the Object
type, which could represent any type
applicable to the type variable E
.
Later we’ll see that this is not always the case. We can place
limitations or bounds on the parameter types, and,
when we do, the compiler can be more restrictive about the erasure of
the type. We’ll explain in more detail later after we discuss bounds,
but, for example:
class
Bounded
<
E
extends
Date
>
{
public
void
addElement
(
E
element
)
{
...
}
}
This parameter type declaration says that the element type
E
must be a subtype of the Date
type. In this case, the erasure of the
addElement()
method is therefore more
restrictive than Object
, and the
compiler uses Date
:
public
void
addElement
(
Date
element
)
{
...
}
Date
is called the
upper bound of this type, meaning that it is the
top of the object hierarchy here and the type can be instantiated only
on type Date
or on “lower” (more
derived) types.
Now that we have a handle on what generic types really are, we can go into a little more detail about how they behave.
[22] For those of you who might like some context for the title of this section, here is where it comes from:
Boy: Do not try and bend the spoon. That’s impossible. Instead, only try to realize the truth.
Neo: What truth?
Boy: There is no spoon.
Neo: There is no spoon?
Boy: Then you’ll see that it is not the spoon that bends, it is only yourself.
—Wachowski, Andy and Larry. The Matrix. 136 minutes. Warner Brothers, 1999.
[23] When generics were added in Java 5.0, things were
carefully arranged such that the raw type of all of the generic
classes worked out to be exactly the same as the earlier, nongeneric
types. So the raw type of a List
in Java 5.0 is the same as the old, nongeneric List
type that had been around since JDK
1.2. Since the vast majority of current Java code at the time did
not use generics, this type equivalency and compatibility was very
important.
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.