Section 8.3.


8.3. "There Is No Spoon"

In the movie The Matrix, [*] the hero Nero 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.

[*] 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.

Nero: What truth?

Boy: There is no spoon.

Nero: 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. 1 videocassete(136 minutes). Warner Brothers, 1999.

The design goals for Java generics were formidable: add a radical new syntax to the language that introduces parameterized types, safely, 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 fairly amazing that these conditions could be satisfied at all and no surprise that it took a while. But as always, the compromises 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 really have 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, allowing 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 actually know anything about generics at all.

8.3.1. 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 looking 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 it 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>

8.3.2. Raw Types

Although the compiler treats different parameterizations of a generic type as truly 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 in which all of the generic type information has been removed and the type variables replaced by some general Java type like Object.

Java 5.0 has been carefully arranged such that the raw type of all of the generic classes works 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 has been around since JDK 1.2. Since the vast majority of current Java code in existence today does not (and may never) use generics, this type equivalency and compatibility is very important.

It is still possible to use raw types in Java 5.0 just as before. The only difference is that the Java 5.0 compiler generates a new type of warning wherever they are used in an "unsafe" way. For example:

     // nongeneric Java code using the raw type, same as always     List list = new ArrayList( ); // assignment ok     list.add("foo"); // unchecked warning on usage of raw type

This snippet uses the raw List type just as any good old-fashioned Java code prior to Java 5.0 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 do 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 okay 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 discussed bounds, but, for example:

     class Bounded< E extends Date > {         public void addElement( E element ) { ... }     }

This parameter type declaration says that the element type E may be any subtype of the Date type. In this case, the erasure of the addElement( ) method can be 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.



    Learning Java
    Learning Java
    ISBN: 0596008732
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net