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).