用Java编写的惰性类?

时间:2013-03-13 15:46:58

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

这是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 个答案:

答案 0 :(得分:12)

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

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

只是检查ChildAParent的子类型,因此演员阵容肯定不会失败。它确实知道您处于不稳定状态,因为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返回ChildBmyPrint类型的广告素材已完成。

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