用Java编写的惰性类?

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铸造(从ParentChildB会发生),你会得到一个异常.

故事的寓意:将未经检查的演员异常视为错误,除非你能证明演员总是会成功.


Jav*_*ier 9

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

在运行时,该方法C get不知道类型C(这就是您无法实例化的原因new C()).调用test.<ChildB>get()实际上是一个调用test.get.return (C) new ChildA()转换为return (Object) new ChildA()由于无界类型的擦除CParent(最左的结合).然后,不需要强制转换因为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)


art*_*tol 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]
Run Code Online (Sandbox Code Playgroud)

第一次调用println只使用调用Object版本,因此不需要强制转换.


Zho*_*gYu 5

如果编译时类型检查被未经检查的强制转换所规避,则通过阅读 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)