用Java编写的惰性类?

有人可以告诉我为什么我在这个片段中没有得到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 get() { return (C) new ChildA(); } public static void main(String[] args) { Test test = new Test(); // should throw ClassCastException... System.out.println(test.get()); // throws ClassCastException... System.out.println(test.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 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) 

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

在运行时,方法C get不知道C的类型(这就是为什么你不能实例化new C() )。 调用test.get()实际上是test.get的调用。 return (C) new ChildA()被转换为return (Object) new ChildA()因为无界类型C的擦除是Parent (它的最左边界)。 然后,不需要println ,因为println需要一个Object作为参数。

另一方面test.get().toString()失败,因为test.get()在调用toString()之前被转换为ChildB

请注意,像myPrint(test.get())这样的调用也会失败。 当调用myPrint时,由get到类型ChildB返回的Parent ChildB完成。

 public static void myPrint(ChildB child) { System.out.println(child); } 

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

 public  C get() { return (C) new ChildA(); } 

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

现在为什么get()不会失败: C类型参数没有运行时组件; 在编译之后,类型参数简单地从程序中删除并替换为其上限( Parent )。 因此,即使type参数与ChildA不兼容,调用也会成功,但是当你第一次尝试使用get()的结果作为ChildB时,会发生一个ChildB (从ParentChildB )并且你会得到一个exception。

故事的寓意:将未经检查的演员exception视为错误,除非您能够向自己certificate演员阵容将永远成功。

查看生成的字节码:

 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版本,因此不需要强制转换。

如果编译时类型检查是通过未经检查的强制转换来规避的,那么从读取JLS开始,何时应该进行运行时类型检查还不清楚。 我想允许编译器假设类型是合理的,并且它可以尽可能晚地延迟运行时检查。 这是一个坏消息,因为它取决于每个编译器的特性,因此程序的行为没有很好地定义。

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

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

我们不能在编译器上做任何错误,这是完全合法的。

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

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

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


第二个println的行为也是未定义的。 编译器没有问题推断toString()是来自超类的方法,因此它不需要强制转换为子类

 Parent tmp = test.get(); String str = tmp.toString(); System.out.println(str);