More fun with Generics – Class.cast()

Continuing the discussion from here and here.

We’ve been making extensive use of generics at work and in the process have discovered a bug in the 1.6 JDK (more on that later). The problem is that we can’t cast to the actual type of some generic instance variables, and also can’t use instanceof. The annoying thing is that it works fine in Eclipse – it’s only an issue in Sun’s JDK, so it fails during a command-line compile. Also it works fine in 1.5, so being one of the few on the team to use 1.6, I end up needing to fix things since it doesn’t affect the rest of the team or the continuous build.

We’ve worked around it by replacing casts with the Class.cast() method:

public T cast(Object obj) {
   if (obj != null && !isInstance(obj))
      throw new ClassCastException();
   return (T) obj;
}

and replace instanceof checks with Class.isAssignableFrom().

But take a look at the cast method. If we were to take the optimistic approach assuming no improper usage and remove the null and isInstance checks, then the method becomes

public T cast(Object obj) {
   return (T) obj;
}

Huh. That’s odd – we’re just replacing a cast with a cast.

So I wrote a simple test method to see why this works:

@SuppressWarnings("unchecked")
public static <T> T cast(@SuppressWarnings("unused") final Class<T> clazz,
       final Object o) {
  return (T)o;
}

(It turns out that the clazz parameter isn’t necessary, but at first I thought it was to justify the T type.)

I wrote some test code:

public static void main(final String... args) {
  List<String> list = new ArrayList<String>();

  ArrayList<String> c1 = cast(ArrayList.class, list);
  System.out.println("c1: " + c1.getClass().getName());

  Object c2 = cast(String.class, list);
  System.out.println("c2: " + c2.getClass().getName());

  String c3 = cast(String.class, list);
  System.out.println("c3: " + c3.getClass().getName());
}

and was surprised that the cast using String.class compiled – I can cast to any class. Of course it fails at runtime – the cast isn’t legal, so the output is

c1: java.util.ArrayList
c2: java.util.ArrayList
Exception in thread "main" java.lang.ClassCastException:
 java.util.ArrayList cannot be cast to java.lang.String

Looking at the decompiled code makes it clear what’s going on – it’s just erasure. The type of T is only used by the compiler to ensure (to the extent possible) that the code is valid, but the runtime type still has to be valid:

public static Object cast(Class clazz, Object o) {
  return o;
}

public static void main(String[] args) {
  List list = new ArrayList();
  ArrayList c1 = (ArrayList)cast(ArrayList.class, list);
  System.out.println((new StringBuilder("c1: ")).append(
         c1.getClass().getName()).toString());
  Object c2 = cast(String.class, list);
  System.out.println((new StringBuilder("c2: ")).append(
         c2.getClass().getName()).toString());
  String c3 = (String)cast(String.class, list);
  System.out.println((new StringBuilder("c3: ")).append(
         c3.getClass().getName()).toString());
}

Of course the Class parameter isn’t necessary:

@SuppressWarnings("unchecked")
public static <T> T cast(final Object o) {
  return (T)o;
}

public static void main(final String... args) {
  List<String> list = new ArrayList<String>();

  ArrayList<String> c1 = cast(list);
  System.out.println("c1: " + c1.getClass().getName());

  Object c2 = cast(list);
  System.out.println("c2: " + c2.getClass().getName());

  String c3 = cast(list);
  System.out.println("c3: " + c3.getClass().getName());
}

would have worked fine.

Comments are closed.

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.