19

ClassCastException有人可以告诉我为什么我在这个片段中没有得到 a吗?我对为什么它没有像我预期的那样工作非常感兴趣。在这一点上,我不在乎这是否是糟糕的设计。

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());
  }
}

这是 java 版本、编译和运行输出:

$ 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)
4

4 回答 4

13

这是由于类型擦除。编译时,编译时

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

只需检查它ChildA是的子类型,Parent因此强制转换肯定不会失败。它确实知道您处于不稳定的状态,因为它ChildA可能无法分配给 type C,因此它会发出未经检查的强制转换警告,让您知道可能会出错。(为什么它允许代码编译,而不仅仅是拒绝它?语言设计选择的动机是 Java 程序员需要以最少的重写迁移他们的旧的预泛型代码。)

现在至于为什么不会失败:类型参数get()没有运行时组件;C编译后,类型参数简单地从程序中删除并替换为其上限 ( Parent)。因此,即使类型参数与 不兼容,调用也会成功ChildA,但是当您第一次实际尝试将结果get()用作强制转换ChildB(from Parentto ChildB) 时,会发生异常,并且会出现异常。

故事的寓意:将未经检查的强制转换异常视为错误,除非您可以向自己证明强制转换总是会成功。

于 2013-03-13T15:59:09.607 回答
10

类型擦除:泛型只是被编译器删除(出于兼容性原因)并在需要时由强制转换替换的语法特性。

在运行时,该方法C get不知道的类型C(这就是你不能实例化的原因new C())。的调用test.<ChildB>get()实际上是 的调用test.getreturn (C) new ChildA()被转换为,return (Object) new ChildA()因为无界类型的擦除CParent(它的最左边的边界)。然后,不需要强制转换,因为println需要Objectas 参数。

另一方面test.<ChildB>get().toString()失败,因为test.<ChildB>get()ChildB调用toString().

请注意,类似的调用myPrint(test.<ChildB>get())也会失败。从Parent返回的get类型转换ChildB为在myPrint调用时完成。

public static void myPrint(ChildB child) {
  System.out.println(child);
}
于 2013-03-13T16:03:36.340 回答
6

查看生成的字节码:

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]

第一次调用println只使用调用的Object版本,因此不需要强制转换。

于 2013-03-13T16:03:46.073 回答
5

如果编译时类型检查被未经检查的强制转换规避,那么从阅读 JLS 中不清楚何时应该进行运行时类型检查。我猜编译器可以假设类型是正确的,并且它可以尽可能晚地延迟运行时检查。这是一个坏消息,因为它取决于每个编译器的特性,因此程序的行为没有得到很好的定义。

显然,编译器将第一个转换println

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

我们不能将任何错误归咎于编译器,这是完全合法的。

编译器还可以将代码转换为

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

因此,对于这样一个简单的程序,JLS 未定义运行时行为。


2nd 的行为println也未定义。编译器可以毫无问题地推断出它toString()是来自超类的方法,因此它不需要强制转换为子类

Parent tmp = test.<ChildB>get();  
String str = tmp.toString();
System.out.println(str);
于 2013-03-13T16:30:00.587 回答