Wednesday, December 13, 2006

Reified Generics: What I Think I Know (but might be wrong)

interface GenericInterface<T> {
  T getValue();

}


How do you create a type safe EasyMock object for the above interface?

GenericInterface<Integer> myInt = EasyMock.createMock(GenericInterface.class);

The above code produces an unchecked assignment warning (although does execute with no errors). The problem has to do with erasure.

Similarly, the following code will not compile:

public class ReifiedGenericTest {
  private List<String> strings;
  private List<Integer> integers;
  public void add(List<String> moreStrings) {
    strings.addAll(moreStrings);
  }
  public void add(List<Integer> moreIntegers) {
    integers.addAll(moreIntegers);
  }
}

Can you explain why? Here is the compiler error:

Name clash: add(java.util.List<java.lang.String>) and add(java.util.List<java.lang.Integer>) have the same erasure

At runtime, the methods have the same signature… the <String> and <Integer> type information is not persisted to run-time, it is only available at compile time. Removing this type information during the compile is called erasure. The term reified means that this type information is not available at run-time.

What else is illegal and what are the other implications of this?

You can't use instanceof to test if an object is of type T:

if (object instanceof T) //will not compile

Or use instanceof to test if what an object’s generic type is:

if (object instanceof List<String>) //will not compile

For a type parameter T, you can't write a class literal T.class:

Class<T> myClass = T.class; //will not compile

Or create an array of List<String>:

List[] array = new List<String>[10]; //will not compile

What’s Going on Here?
I hope at this point you have begun to realize that generics, as implemented in Java 5, have some substantial limitations. Why is this so?

Generic type parameters are not reified: they are not available at runtime. Generics are implemented using erasure, in which generic type parameters are simply removed at runtime.

Generics are implemented using erasure in order to support migration compatibility. Adding the generics feature in Java 5 could not break any existing code and needed to be compatible with Java 4. This meant erasing the type information at run-time and retrofitting the Collections API to use erased generics. The alternative would have been to create a second collections API in parallel that used reified types, and maintain both the new API and the old API (which was the approach taken when C# introduced generics). So there was a choice: use erasure and the existing collections API or use reification and write a new collections API. Sun chose the first option.

Java 7 to the Rescue (maybe)
At this time, there is no JSR to eliminate type erasure in Java 7, but there seems to be a healthy debate. There are two proposed approaches to adding reification to types: use the existing language to add run time type information or modify the language by adding some sort of reification flag.

Upgrading the existing language would break both source compatibility and binary compatibility. But more dangerously, runtime exceptions would result when existing generic objects are used in unsafe ways, such as casting a List<Object> to a List<String>.

The alternative, extending the language to identify reified types from erased types, would be straightforward to implement. You could do it with annotations or reusing an existing keyword (a cringe inducing habit of Sun’s, in my opinion). The downside is that a reified version of the Collections API would need to be produced and maintained alongside the existing one (just like C# did when they introduced generics). This might be a substantial amount of work. Implementing a change this way would require the least amount of work for Java programmers, but the language team, VM teams, and compiler teams would all be burdened with making these changes.

So What's the Solution?
Back to the original example, what do you do when you need to make an EasyMock object for a generified interface? There are 3 options
1. Live with the unchecked warning:
GenericInterface<Integer> myInt = EasyMock.createMock(GenericInterface.class);
2. Do not type the generic when testing it:
GenericInterface myInt = EasyMock.createMock(GenericInterface.class);
3. Embed the type in an implementation:
MockGenericInterface implements GenericInterface<Integer> {
  public Integer getValue( ) {

    //…
}


In practice, I find that I need a type in my unit test, so option #2 doesn’t appeal to me. I find option #3 much more verbose than option #1, so it doesn’t appeal to me either. I’d say it is up to you whether you want to suppress the compiler warning using a directive!

Links http://gafter.blogspot.com/2006/11/reified-generics-for-java.html (excellent blog BTW!)
http://tech.puredanger.com/java7#reified
http://www.weiqigao.com/blog/2007/01/20/java_generics_let_the_other_shoe_drop.html

No comments: