有人可以告诉我为什么我在这个片段中没有得到ClassCastException
吗?我非常感兴趣的是它为什么不像我期望的那样工作。我现在不在乎这是不是很糟糕的设计。
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)
答案 0 :(得分:12)
这是由于类型擦除。在编译时,编译时
public <C extends Parent> C get() {
return (C) new ChildA();
}
只是检查ChildA
是Parent
的子类型,因此演员阵容肯定不会失败。它确实知道您处于不稳定状态,因为ChildA
可能无法分配给C
类型,因此会发出未经检查的警告,让您知道某些内容可能出错。 (为什么它允许代码编译,而不是仅仅拒绝它?语言设计选择的动机是Java程序员需要以最少的重写来迁移旧的预泛化代码。)
现在为什么get()
没有失败:C
类型参数没有运行时组件;在编译之后,类型参数简单地从程序中删除并替换为其上限(Parent
)。因此,即使type参数与ChildA
不兼容,调用也会成功,但是第一次实际尝试使用get()
的结果作为ChildB
强制转换(来自{{1} }} Parent
}将发生,你会得到一个例外。
故事的寓意:将未经检查的演员例外视为错误,除非你能证明演员总是会成功。
答案 1 :(得分:9)
Type erasure:泛型只是一种语法特性,由编译器删除(出于兼容性原因),并由强制转换替换为。
在运行时,方法C get
不知道C
的类型(这就是为什么你不能实例化new C()
)。调用test.<ChildB>get()
实际上是对test.get
的调用。 return (C) new ChildA()
被转换为return (Object) new ChildA()
,因为无界类型C
的删除是Parent
(最左边界)。然后,不需要强制转换,因为println
期望Object
作为参数。
另一方面,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);
}
答案 2 :(得分: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
版本的电话,因此不需要演员。
答案 3 :(得分:4)
如果编译时类型检查被未经检查的强制转换规避,那么从读取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未定义运行时行为。
第二个println
的行为也未定义。编译器没有问题推断toString()
是来自超类的方法,因此它不需要转换为子类
Parent tmp = test.<ChildB>get();
String str = tmp.toString();
System.out.println(str);