I've fiddled with my blog template because I decided I wanted more horizontal viewing space, given that it was using less than a third of my 1920 horizontal pixels. If it feels too spread out for you, I added a drag-and-drop handle over to the left to let you resize the main content column. The javascript is pretty primitive. If it breaks, drop me a comment.
>
>
>
>

Thursday, February 19, 2009

Generic Non-Erasure?

I was coding away the other day and got this error:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
on code that looks in essence like this:
public class Example {
    public static void main(String[] args) {
        List<String> stringList = legacyGetList();
        System.out.println("First element: " + stringList.get(0));
    }

    static List legacyGetList() {
        List untypedList = new ArrayList();
        untypedList.add(new Integer(5));
        return untypedList;
    }
}
It happens on the fourth line, the System.out.println. What's the deal? It can't be related to the type parameter, can it? Generic types are erased at runtime, right? Well, it turns out that if you change List<String> to List<Object> or just List, then it works. Why?
Because of a compiler optimization. String concatenation compiles into StringBuilder invocations (at least it has since StringBuilder was introduced). StringBuilder has several overloaded append(...) methods--one for each primitive type, one for java.lang.String, and one for java.lang.Object--for convenience and, I guess, performance. So ask yourself: when is a method selected for execution from a list of overloads? If you answered "at compile-time", then you're right. If you answered the other way, prove it to yourself:
public class AnotherExample {
    public static void main(String[] args) {
        foo((String) null);
        foo((Object) null);
    }

    static void foo(String s) {
        System.out.println("It's a string");
    }

    static void foo(Object o) {
        System.out.println("It's an object");
    }
}
This fact answers the "Why?" from above. In the example, the compiler knows that the List is a List<String>, and therefore when the string concatenation is compiled into StringBuilder invocations, it compiles to StringBuilder.append(String) instead of StringBuilder.append(Object). Then at runtime, a java.lang.Integer is passed to the append(String) method, which obviously won't work. The proof is in the disassembled class code. Disassembled code from the example above:
0:   invokestatic    #16; //Method legacyGetList:()Ljava/util/List;
3:   astore_1
4:   getstatic       #20; //Field java/lang/System.out:Ljava/io/PrintStream;
7:   new     #26; //class java/lang/StringBuilder
10:  dup
11:  ldc     #28; //String First element:
13:  invokespecial   #30; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
16:  aload_1
17:  iconst_0
18:  invokeinterface #33,  2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
23:  checkcast       #39; //class java/lang/String
26:  invokevirtual   #41; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29:  invokevirtual   #45; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32:  invokevirtual   #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
35:  return
Note the "26: invoke StringBuilder.append(String)". That's the bad method call I mentioned. You'll notice the compiler throws a checkcast on the line before, which is where our ClassCastException comes from. If we change the List<String> to List<Object>, we get:
0:   invokestatic    #16; //Method legacyGetList:()Ljava/util/List;
3:   astore_1
4:   getstatic       #20; //Field java/lang/System.out:Ljava/io/PrintStream;
7:   new     #26; //class java/lang/StringBuilder
10:  dup
11:  ldc     #28; //String First element:
13:  invokespecial   #30; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
16:  aload_1
17:  iconst_0
18:  invokeinterface #33,  2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
23:  invokevirtual   #39; //Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
26:  invokevirtual   #43; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29:  invokevirtual   #47; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
32:  return
The checkcast is gone, presumably because there's no reason to check if something is castable to Object, and it now correctly calls StringBuilder.append(Object).