Generics: Beauty and Madness

Posted on Posted in Java, Stories

Generics : Beauty and Madness

Generic types added in JDK 5.0 provided enhancement to type safety in Java. For example in using Collections, generic types allow restricting the type the collection would contain as opposed to pre-JDK5 wherein collection classes accept anything that extends from java.lang.Object (which means almost everything!). And this improvement of allowing types to be specified for collections reduces, if not eliminate, the need for casts and also reduces unexpected ClassCastException at runtime.

Generics also became a key element in Generic DAOs (Data Access Objects) used for ORM driven applications such as those using Hibernate or Toplink.

As beautiful as it may seem, Generics also has its sharp corners which will surely give you several head scratching or even cursing if you are not aware of these Generics traps. The following are some important things to remember when using Generics.

Generics are not covariant.

Most developers might be tempted to think that collections are somehow the better incarnation of arrays and applying the concept of inheritance, some idioms with arrays may sometimes be applied to parameterized collections which turns out not to work the way you think it is.

For example, we all know that arrays are objects. And supposed you have an array of Integer, and since Integer extends Number this is perfectly legal:

Number[] numbers = new Integer[3];

and since all objects in Java extends from java.lang.Object this is also perfectly legal:

Object[] numbers = new Integer[3];

This idiom however if applied to parameterized collections won’t work. The following would not compile.

List numbers = new ArrayList();

Why? Because if the compiler would allow this idiom, it would break the promise of type-safety implicit in the definition of the ArrayList declaration, it is an ArrayList of Integer. That is why Generics cannot be covariant.

Problem with Erasure

The implementation of Generics in Java often described as a crappy implementation is somehow tricky. This implementation is known as type erasure, type-safety is enforced by the compiler at compile time but erases these type information before creating the bytecodes. Because of erasure List and List are considered the same class (at runtime the type parameters are already erased and both just exists as raw Lists).

Since compiler removes all type information before the byte codes are generated, suppose you have a class class MyClass { }, these codes will not compile:

public void someMethod(Object item) {
     if (item instanceof E) { . . .} //Fails. Cannot determine type of E

     E e = new E();                  //no way to determine a constructor
     E[] eArray = new E[3];      //fails
}

Constructing Wildcard References

Java allow us to use wildcards in collections when we don’t know what the collection will hold at runtime by doing for example: Set<?> setOfUnknown as opposed to raw types. Supposed you want to create a copy of a collection inside a method, and that collection happens to be using a wildcard, So you might think this is ok:

public List<?> copyList(List<?> list) {
     return new ArrayList<?>(list);
}

This is illegal! However, this is fine:

public List<?> copyList(List<?> list) {
     return new ArrayList(list);
}

Static Methods of Generic Classes Cannot Reference the Type Parameters of their Enclosing Generic Class
class SomeClass {
     public static void doSomethingWithT(T t) { } //error
}

ExerciseThere are still some other sharp edges you would encounter when using Generics, unfortunately we cannot discuss them all. So before I formally end this article, let's exercise our minds with this little puzzle.What would be the output of this program?

public class GenericClass {
     public void displayAll(Collection<?> collection) {
          String output = "";
          for(Object o : collection) {
              output += o;
          }
          System.out.println(output);
     }

     public void displayAll(List ints) {
          String output = "";
          for(int i : ints) {
              output += i;
          }
          System.out.println(output);
     }

     public static void main(String[] args) {
          List strings = Arrays.asList("1", "2", "3");
          new GenericClass().displayAll(strings);
     }
}

It may seem that this would print "123" but this would actually throw a ClassCastException at runtime.Where did the ClassCastException came from?If you will take a closer look, the methods of class GenericClass declares generic types but the way GenericClass was instantiated inside the main() method, the class was declared without specifying a type for its generic type parameter.So class GenericClass was instantiated as a raw type. And using raw types Erases ALL generic type information. Remember that, ALL generic type information are erased. So the wildcard "?" in Collection<?> is erased leaving it as a raw Collection, and the Integer type parameter at List is erased leaving it also as a raw List.Now, the call to displayAll(strings) which if we have declared the type for the generic class during instantiation would have called the method displayAll(Collection<?> collection), since the class was instantiated as raw type, and type information are erased from the method, the runtime will look for the most specific overload of displayAll() thus calling displayAll(List ints). And inside that method, we looped through ints expecting the Integer to be auto-unboxed to int, but since type is erased, instead of List of integers, ints actually contains Strings, and when an attempt to unbox String to int, a ClassCastException is thrown.Another problem with this code is that it declares a generic type T, but the generic type is never used inside the class.Actually if you will pay attention to your compiler, compiling this code will actually give us a warning.

Type safety: The method displayAll(List) belongs to the raw type GenericClass. References to generic type GenericClass should be parameterized.

The warning suggests the the method displayAll() which accepts a List argument would be called.Summary

  • While Generics offer type-safety, we must remember that Java's Generics feature is implemented at compile-time. At runtime, type information are erased; so don't expect to use generic types the way you would normally do with non-generic types.
  • Dont' use raw types in new code.
  • Generic classes should be instantiated the way they are meant to and never instantiate them as raw types as it erases ALL type information.
  • Auto-unboxing happens and hurts when you least expect it.
  • Do not ignore compiler warnings.

Reference

  • Google I/O Session: Java Puzzlers Presented by Josh Bloch and Jeremy Manson - YouTube Video

One thought on “Generics: Beauty and Madness

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.