19

Can someone please enlighten me as to why I don't get a ClassCastException in this snippet? I'm strictly interested into why it isn't working as I was expecting. I don't care at this point whether this is bad design or not.

public class Test {
  static class Parent {
    @Override
    public String toString() { return "parent"; }
  }

  static class ChildA extends Parent {
    @Override
    public String toString() { return "child A"; }
  }

  static class ChildB extends Parent {
    @Override
    public String toString() { return "child B"; }
  }

  public <C extends Parent> C get() {
    return (C) new ChildA();
  }

  public static void main(String[] args) {
    Test test = new Test();

    // should throw ClassCastException...
    System.out.println(test.<ChildB>get());

    // throws ClassCastException...
    System.out.println(test.<ChildB>get().toString());
  }
}

This is the java version, compilation, and run output:

$ java -version
java version "1.7.0_17"
Java(TM) SE Runtime Environment (build 1.7.0_17-b02)
Java HotSpot(TM) 64-Bit Server VM (build 23.7-b01, mixed mode)
$ javac -Xlint:unchecked Test.java
Test.java:24: warning: [unchecked] unchecked cast
    return (C) new ChildA();
               ^
  required: C
  found:    ChildA
  where C is a type-variable:
    C extends Parent declared in method <C>get()
1 warning
$ java Test
child A
Exception in thread "main" java.lang.ClassCastException: Test$ChildA cannot be cast to Test$ChildB
  at Test.main(Test.java:30)
Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
  • I don't have an answer but I'm reminded of this earlier question/answer, maybe it will help http://stackoverflow.com/q/6538058/630384 – DHall Mar 13 '13 at 15:56
  • 2
    There's a reason why the unchecked cast warning is there. That's literally what it means, that the cast won't/can't cause a runtime check. – millimoose Mar 13 '13 at 16:54

4 Answers4

13

This is due to type erasure. At compile time, when compiling

public <C extends Parent> C get() {
  return (C) new ChildA();
}

simply checks that ChildA is a subtype of Parent and thus the cast won't definitely fail. It does know that you're on shaky ground, given that ChildA might not be assignable to type C, so it issues an unchecked-cast warning letting you know that something could go wrong. (Why does it allow the code to compile, rather than just rejecting it? Language design choice motivated by the need for Java programmers to migrate their old pre-generics code with a minimum of rewriting.)

Now as to why get() doesn't fail: there is no runtime component to the C type parameter; after compilation the type argument simply gets erased out of the program and replaced with its upper bound (Parent). So the call will succeed even if the type argument is incompatible with ChildA, but the first time you actually try to use the result of get() as a ChildB a cast (from Parent to ChildB) will occur and you'll get an exception.

The moral of the story: treat unchecked cast exceptions as errors, unless you can prove to yourself that the cast would always succeed.

jacobm
  • 13,790
  • 1
  • 25
  • 27
10

Type erasure: generics is only a syntactic feature that is removed by the compiler (for compatibility reasons) and replaced by casts where required.

In runtime, the method C get doesn't know the type of C (that is why you cannot instantiate new C()). An invocation of test.<ChildB>get() is actually an invocation of test.get. return (C) new ChildA() is converted to return (Object) new ChildA() because the erasure of the unbounded type C is Parent (its leftmost bound). Then, no cast is required because println expects an Object as argument.

On the other hand test.<ChildB>get().toString() fails, because test.<ChildB>get() is casted to ChildB before invoking toString().

Note that an invocation like myPrint(test.<ChildB>get()) will also fail. The cast from the Parent returned by get to type ChildB is done when myPrint is invoked.

public static void myPrint(ChildB child) {
  System.out.println(child);
}
Javier
  • 12,100
  • 5
  • 46
  • 57
  • I'm marking this answer as correct because, while I knew about erasure, this answer connected the dots and helped me realize when casts are actually being introduced in the compiled code (e.g. `myPrint`). Thanks! – Ionuț G. Stan Mar 13 '13 at 16:27
  • I still find it interesting that the first call doesn't introduce a cast check, while the second does, although it's just calling `toString` which is available on `Object`. No `ChildB` specific method. – Ionuț G. Stan Mar 13 '13 at 16:40
  • If generics are removed upon compilation, then why can I give you a `.jar` to compile from with generic types? There must be some trace of them, even if they are not used during runtime. – Ryan Amos Mar 13 '13 at 21:00
  • 1
    @RyanAmos Generic type info is kept for class members (fields, methods, etc.) but not instances. – Paul Bellora Mar 13 '13 at 21:03
  • 1
    @IonuțG.Stan But it's not calling `toString` on `Object`, it's calling it on `ChildB`. Consider that this doesn't except: `System.out.println(((Object)Main. get()).toString());`. – Paul Bellora Mar 13 '13 at 21:24
  • @RyanAmos you are right: the generic type information is not available **at runtime**. – Javier Mar 14 '13 at 01:33
6

Look at the generated bytecode:

12  invokevirtual Test.get() : Test$Parent [30]
15  invokevirtual java.io.PrintStream.println(java.lang.Object) : void [32]
18  getstatic java.lang.System.out : java.io.PrintStream [24]
21  aload_1 [test]
22  invokevirtual Test.get() : Test$Parent [30]
25  checkcast Test$ChildB [38]
28  invokevirtual Test$ChildB.toString() : java.lang.String [40]
31  invokevirtual java.io.PrintStream.println(java.lang.String) : void [44]

The first call to println just uses the Object version of the call so no cast is necessary.

artbristol
  • 32,010
  • 5
  • 70
  • 103
5

If compile time type checking is circumvented by unchecked cast, it's unclear, from reading JLS, when should the runtime type checking occur. I guess the compiler is allowed to assume that types are sound, and it can delay runtime check as late as possible. This is a bad news, since it depends on the idiosyncrasy of each compiler, therefore the behavior of the program is not well defined.

Apparently, the compiler transforms the first println as

Parent tmp = test.<ChildB>get();  // ok at runtime
System.out.println(tmp);

We cannot place any fault on the compiler to do that, it's perfectly legit.

The compiler can also transform the code to

ChildB tmp = test.<ChildB>get();  // fail at runtime
System.out.println(tmp);

Therefore for such a simple program, the runtime behavior is undefined by JLS.


The behavior of the 2nd println is also undefined. The compiler has no problem to deduce that toString() is a method from a superclass, therefore it doesn't need the cast to the subclass

Parent tmp = test.<ChildB>get();  
String str = tmp.toString();
System.out.println(str);
ZhongYu
  • 19,446
  • 5
  • 33
  • 61