“There Is No Spoon”

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.

Erasure

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>

Raw Types

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.