Ion*_*tan 18 java generics casting
有人可以告诉我为什么我没有得到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());
}
}
Run Code Online (Sandbox Code Playgroud)
这是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)
Run Code Online (Sandbox Code Playgroud)
jac*_*obm 12
这是由于类型擦除.在编译时,编译时
public <C extends Parent> C get() {
return (C) new ChildA();
}
Run Code Online (Sandbox Code Playgroud)
只需检查这ChildA是一个子类型,Parent因此演员阵容肯定不会失败.它确实知道你处于不稳定状态,因为它ChildA可能无法分配给类型C,所以它会发出一个未经检查的警告,让你知道某些事情可能出错.(为什么它允许代码编译,而不是仅仅拒绝它?语言设计选择的动机是Java程序员需要以最少的重写来迁移旧的预泛化代码.)
现在为什么get()不会失败:Ctype参数没有运行时组件; 在编译之后,类型参数简单地从程序中删除并替换为其上限(Parent).因此,调用将成功,即使类型参数是不兼容ChildA的,但你第一次真正尝试使用结果get()作为ChildB铸造(从Parent到ChildB会发生),你会得到一个异常.
故事的寓意:将未经检查的演员异常视为错误,除非你能证明演员总是会成功.
类型擦除:泛型只是编译器删除的语法特性(出于兼容性原因),并在需要时由强制转换替换.
在运行时,该方法C get不知道类型C(这就是您无法实例化的原因new C()).调用test.<ChildB>get()实际上是一个调用test.get.return (C) new ChildA()转换为return (Object) new ChildA()由于无界类型的擦除C是Parent(最左的结合).然后,不需要强制转换因为println期望Objectas参数.
另一方面test.<ChildB>get().toString()失败,因为在调用之前test.<ChildB>get()被转换为.ChildBtoString()
请注意,类似的调用myPrint(test.<ChildB>get())也会失败.从演员Parent的返回get类型为ChildB当完成myPrint被调用.
public static void myPrint(ChildB child) {
System.out.println(child);
}
Run Code Online (Sandbox Code Playgroud)
查看生成的字节码:
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]
Run Code Online (Sandbox Code Playgroud)
第一次调用println只使用调用Object版本,因此不需要强制转换.
如果编译时类型检查被未经检查的强制转换所规避,则通过阅读 JLS 并不清楚何时应该进行运行时类型检查。我想编译器可以假设类型是健全的,并且它可以尽可能晚地延迟运行时检查。这是一个坏消息,因为它取决于每个编译器的特性,因此程序的行为没有得到很好的定义。
显然,编译器将第一个转换println为
Parent tmp = test.<ChildB>get(); // ok at runtime
System.out.println(tmp);
Run Code Online (Sandbox Code Playgroud)
我们不能将任何错误归咎于编译器这样做,这是完全合法的。
编译器还可以将代码转换为
ChildB tmp = test.<ChildB>get(); // fail at runtime
System.out.println(tmp);
Run Code Online (Sandbox Code Playgroud)
因此,对于这样一个简单的程序,JLS 未定义运行时行为。
第二个的行为println也是未定义的。编译器可以毫无问题地推断出这toString()是来自超类的方法,因此不需要强制转换为子类
Parent tmp = test.<ChildB>get();
String str = tmp.toString();
System.out.println(str);
Run Code Online (Sandbox Code Playgroud)