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.