用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.
实际上是test.get
的调用。 return (C) new ChildA()
被转换为return (Object) new ChildA()
因为无界类型C
的擦除是Parent
(它的最左边界)。 然后,不需要println
,因为println
需要一个Object
作为参数。
另一方面test.
失败,因为test.
在调用toString()
之前被转换为ChildB
。
请注意,像myPrint(test.
这样的调用也会失败。 当调用myPrint
时,由get
到类型ChildB
返回的Parent
ChildB
完成。
public static void myPrint(ChildB child) { System.out.println(child); }
这是由于类型擦除。 在编译时,编译时
public C get() { return (C) new ChildA(); }
只需检查ChildA
是Parent
的子类型,因此ChildA
不一定会失败。 它确实知道你处于不稳定状态,因为ChildA
可能无法分配给C
,所以它会发出一个未经检查的警告,让你知道某些事情可能出错。 (为什么它允许代码编译,而不是仅仅拒绝它?语言设计选择的动机是Java程序员需要以最少的重写来迁移旧的预泛化代码。)
现在为什么get()
不会失败: C
类型参数没有运行时组件; 在编译之后,类型参数简单地从程序中删除并替换为其上限( Parent
)。 因此,即使type参数与ChildA
不兼容,调用也会成功,但是当你第一次尝试使用get()
的结果作为ChildB
时,会发生一个ChildB
(从Parent
到ChildB
)并且你会得到一个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);