Generics and Legacy Code

My previous article “Generics in Java” was a mere introduction to the generics. You can say that it was just the tip of the ice burg. This time we will go a bit inside generics and discuss about the issues in integrating generic and non-generic code..

Imagine we have an ArrayList, of type Integer, and we're passing it into a method from a class whose source code we don't have access to. Will this work?

// a Java 5 class using a generic collection
import Java.util.*;
public class TestLegacy {
public static void main(String[] args) {
List myList = new ArrayList();
// type safe collection
myList.add(4);
myList.add(6);
Adder adder = new Adder();
int total = adder.addAll(myList) ;
// pass it to an un-typed argument
System.out.println(total);
}
}

The older, non-generics class we want to use:

import Java.util.*;
class Adder {
int addAll(List list) {
// method with a non-generic List argument,
// but assumes (with no guarantee) that it will be Integers
Iterator it = list.iterator();
int total = 0;
while (it.hasNext()) {
int i = ((Integer)it.next()).intValue();
total + = i;
}
return total;
}
}
Yes, this works just fine. You can mix correct generic code with older non-generic code, and everyone is happy.

In that example, method wasn't doing anything except getting the Integer (using a cast) from the list and accessing its value. So, there was no risk to the caller's code, but the legacy method might have blown up if the list passed in, contained anything but Integers (which would cause a ClassCastException).

But now imagine that you call a legacy method that doesn't just read a value but adds something to the ArrayList? Will this work?

import java.util.*;
public class TestBadLegacy {
public static void main(String[] args) {
List myList = new ArrayList();
myList.add(4);
myList.add(6);
Inserter in = new Inserter();
in.insert(myList); // pass List to legacy code
}
}
class Inserter {
// method with a non-generic List argument
void insert(List list) {
list.add(new Integer(42)); // adds to the incoming list
}
}

Sure, this code works. It compiles, and it runs. The insert() method puts an Integer into the list that was originally typed as , so no problem.

But…what if we modify the insert() method like this:

void insert(List list) {
list.add(new String("42")); // put a String in the list passed in
}

Will that work? Yes, sadly, it does! It both compiles and runs. No runtime exception.

How can that be?

Remember, in the older legacy code i.e. before java 5, you were allowed to put anything into a collection. So, in order to support your legacy code, Java 5 compiler is forced to let you to compile your new type safe code, even though your new code invokes an old method of some pre-generics class, which was totally unaware of the type safety.

In the above example with the legacy insert() method that adds a String, the compiler generated a warning:

javac TestBadLegacy.java
Note: TestBadLegacy.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Remember that compiler warnings are NOT considered a compiler failure. The compiler generated a perfectly valid class file from the compilation, but it was kind enough to tell you by saying, in so many words, "I seriously hope you know what you are doing because this old code has NO respect (or even knowledge) of your typing, and can do whatever the heck it wants to your precious ArrayList<>."

The reason the compiler produces a warning is because the method is ADDING something to the collection! In other words, the compiler knows there's a chance the method might add the wrong thing to a collection the caller thinks is type safe.

There's one Big Truth you need to know to understand why it runs without problems—the JVM has no idea that your ArrayList was supposed to hold only Integers. The typing information does not exist at runtime! All your generic code is strictly for the compiler. Through a process called "type erasure," the compiler does all of its verifications on your generic code and then strips the type information out of the class bytecode. At runtime, ALL collection code—both legacy and new Java 5 code you write using generics—looks exactly like the pre-generic version of collections. None of your typing information exists at runtime. In other words, even though you WROTE

List myList = new ArrayList();

By the time the compiler is done with it, the JVM sees what it always saw before Java 5 and generics:

List myList = new ArrayList();

The compiler even inserts the casts for you—the casts you had to do to get things out of a pre-Java 5 collection.

Always keep in mind, even though you are using generics, no type information is available at runtime. The fact is, you don't NEED runtime protection…until you start mixing up generic and non-generic code.

The only advice we have is to pay very close attention to those compiler warnings:


javac TestBadLegacy.java

Note: TestBadLegacy.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

This compiler warning isn't very descriptive, but the second note suggests that you recompile with -xlint:unchecked. If you do, you'll get something like this:


javac -Xlint:unchecked TestBadLegacy.java

TestBadLegacy.java:17: warning: [unchecked] unchecked call to
add(E) as a member of the raw type java.util.List
list.add(new String("42"));
^
1 warning

When you compile with the -Xlint:unchecked flag, the compiler shows you exactly which method(s) might be doing something dangerous. In this example, since the list argument was not declared with a type, the compiler treats it as legacy code and assumes no risk for what the method puts into the "raw" list.


Just remember that the moment you turn that type safe collection over to older, non-type safe code, your protection vanishes. Again, pay very close attention to compiler warnings, and be prepared to see issues like we just discussed….

References:

• Sun Certified Programmer for Java 5 Study Guide by Kathy Sierra and Bert Bates.

No comments: