Helping ordinary people create extraordinary websites!
GET OUR NEWSLETTER
Your Email:
 

Java Theory and Practice: Generics Gotchas

By Brian Goetz
2005-03-23


Construction delays

Because of erasure, List<Integer> and List<String> are the same class, and the compiler only generates one class when compiling List<V> (unlike in C++). As a result, the compiler doesn't know what type is represented by V when compiling the List<V> class, and so you can't do certain things with a type parameter (the V in List<V>) within the class definition of List<V> that you could do if you knew what class was being represented.

Because the runtime cannot tell a List<String> from a List<Integer> (at runtime, they're both just Lists), constructing variables whose type is identified by a generic type parameter is problematic. This lack of type information at runtime poses a problem for generic container classes and for generic classes that want to make defensive copies.

Consider the generic class Foo:

class Foo<T> { 

public void doSomething(T param) { ... }
}


Suppose the doSomething() method wants to make a defensive copy of the param argument on entry? You don't have many options. You'd like to implement doSomething() like this:

public void doSomething(T param) { 

T copy = new T(param); // illegal
}


But you cannot use a type parameter to access a constructor because, at compile time, you don't know what class is being constructed and therefore what constructors are available. There's no way to express a constraint like "T must have a copy constructor" (or even a no-arg constructor) using generics, so accessing constructors for classes represented by generic type parameters is out.

What about clone()? Let's say Foo was defined to make T extend Cloneable:

class Foo<T extends Cloneable> { 

public void doSomething(T param) {
T copy = (T) param.clone(); // illegal
}
}


Unfortunately, you still can't call param.clone(). Why? Because clone() has protected access in Object and, to call clone(), you have to call it through a reference to a class that has overridden clone() to be public. But T is not known to redeclare clone() as public, so cloning is also out.

Constructing wildcard references
OK, so you can't copy a reference to a type whose class is totally unknown at compile time. What about wildcard types? Suppose you want to make a defensive copy of a parameter whose type is Set<?>. You know that Set has a copy constructor. You've also been told it's better to use Set<?> instead of the raw type Set when you don't know the type of the set's contents, because that approach is likely to emit fewer unchecked conversion warnings. So you try this:

class Foo {

public void doSomething(Set<?> set) {
Set<?> copy = new HashSet<?>(set); // illegal
}
}


Unfortunately, you can't invoke a generic constructor with a wildcard type argument, even though you know such a constructor exists. However, you can do this:

class Foo {

public void doSomething(Set<?> set) {
Set<?> copy = new HashSet<Object>(set);
}
}


This construction is not the most obvious, but it is type-safe and will do what you think new HashSet<?>(set) would do.

Constructing arrays
How would you implement ArrayList<V>? The ArrayList class is supposed to manage an array of V, so you might expect the constructor for ArrayList<V> to create an array of V:

class ArrayList<V> {

private V[] backingArray;
public ArrayList() {
backingArray = new V[DEFAULT_SIZE]; // illegal
}
}


But this code does not work -- you cannot instantiate an array of a type represented by a type parameter. The compiler doesn't know what type V really represents, so it cannot instantiate an array of V.

The Collections classes use an ugly trick to get around this problem, one that generates an unchecked conversion warning when the Collections classes are compiled. The constructor for the real implementation of ArrayList looks likes this:

class ArrayList<V> {

private V[] backingArray;
public ArrayList() {
backingArray = (V[]) new Object[DEFAULT_SIZE];
}
}


Why does this code not generate an ArrayStoreException when backingArray is accessed? After all, you can't assign an array of Object to an array of String. Well, because generics are implemented by erasure, the type of backingArray is actually Object[], because Object is the erasure of V. This means that the class is really expecting backingArray to be an array of Object anyway, but the compiler does extra type checking to ensure that it contains only objects of type V. So this approach will work, but it's ugly, and not really something to emulate (even the authors of the generified Collections framework say so -- see Resources).

An alternate approach would have been to declare backingArray as an array of Object, and cast it to V[] everywhere it is used. You would still get unchecked conversion warnings (as you do with the previous approach), but it would have made some unstated assumptions (such as the fact that backingArray should not escape the implementation of ArrayList) more clear.

The road not taken
The best approach would be to pass a class literal (Foo.class) into the constructor, so that the implementation could know, at runtime, the value of T. The reason this approach was not taken was for backward compatibility -- then the new generified collection classes would not be compatible with previous versions of the Collections framework.

Here's how ArrayList would have looked under this approach:

public class ArrayList<V> implements List<V> {

private V[] backingArray;
private Class<V> elementType;

public ArrayList(Class<V> elementType) {
this.elementType = elementType;
backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH);
}
}


But wait! There's still an ugly, unchecked cast there when calling Array.newInstance(). Why? Again, it's because of backward compatibility. The signature of Array.newInstance() is:

public static Object newInstance(Class<?> componentType, int length)

instead of the type-safe:

public static<T> T[] newInstance(Class<T> componentType, int length)

Why was Array generified this way? Again, the frustrating answer is to preserve backward compatibility. To create an array of a primitive type, such as int[], you call Array.newInstance() with the TYPE field from the appropriate wrapper class (in the case of int, you would pass Integer.TYPE as the class literal). Generifying Array.newInstance() with a parameter of Class<T> instead of Class<?> would have been more type-safe for reference types, but would have made it impossible to use Array.newInstance() to create an instance of a primitive array. Perhaps in the future, an alternate version of newInstance() will be provided for reference types so you can have it both ways.

You may see a pattern beginning to emerge here -- many of the problems, or compromises, associated with generics are not issues with generics themselves, but side effects of the requirement to preserve backward compatibility with existing code.

Tutorial Pages:
» Identify and avoid some of the pitfalls in learning to use generics
» Generics are not covariant
» Construction delays
» Generifying existing classes
» Implications of erasure
» Summary
» Resources


First published by IBM developerWorks


 | Bookmark
Related Tutorials:
» All about JAXP, Part 1
» Make Database Queries Without the Database
» Load List Values for Improved Efficiency
» 2 Ways To Implement Session Tracking
» A Simple Way to Read an XML File in Java
» Develop Aspect-Oriented Java Applications with Eclipse and AJDT

Advertise with Us!


Tutorials Scripts Web Hosting Developer Manuals
Resources