Java 继承转换运行时错误与编译器错误

Java inheritance casting runtime error vs compiler error

问题

考虑下面的代码。 B继承自A,其中none继承自String

我想知道以下问题的答案:

  1. 为什么第一次转换 B b1 = (B) a1; 会产生运行时错误?
  2. 为什么第一个第二个转换 String b1 = (String) a1; 会产生编译错误?
  3. 为什么会有差异?为什么编译器在第一种情况下看不到问题?

代码

public class Main {
    public static void main(String[] args) {
        A a1 = new A();
        B b1 = (B) a1;
        
        String b1 = (String) a1;
    }

}

与 class A:

public class A {}

和class B:

public class B extends A {}

  1. why does the first first cast, B b1 = (B) a1;, produce a runtime error?

A 类型的变量可以存储 B 的实例。但是,并非 A 的所有实例都是 B.

的实例

A 不是 B,但 BA。 (比如,不是所有的动物都是狗,但狗是动物)。

  1. why does the first second cast, String b1 = (String) a1;, produce a compilation error?

A 不是 String 的超类型,并且 String 不是 B.

的超类型
  1. Why is there a difference? Why doesn't the compiler see the problem in the first case?

因为A类型的变量可以存储B的实例;但是 A 类型的变量永远不能存储 String.

的实例

A 类型的变量实际上可以是 B 类型,因为 B 扩展了 A。但是 A 类型的变量永远不可能是 String 类型.这就是为什么编译器可以捕捉到 String 的转换,但不能捕捉到 B.

的转换

why does the first first cast, B b1 = (B) a1;, produce a runtime error?

因为 a1A 的实例,但与 B 不兼容。具体来说,new A() 创建了一个与 A 的子 class 不兼容的 object。如果 object 的运行时 class(即调用 new 的 class)与目标的子 class 不同或不同class,投射到该目标 class 将在运行时失败。这仅仅是因为 child class 与 object.

无关

why does the first second cast, String b1 = (String) a1;, produce a compilation error?

即使实际转换发生在运行时,编译器也会执行类型检查并防止像这样的无意义操作。对于这种情况,将 A object 转换为 String 是无意义的,编译器可以检测到:StringA 之间没有关系,并且编译器知道什么 class 是什么 class 的 child。编译器知道 Java 中没有办法让 a1 成为 StringString 的子 class 的实例,那是因为 String 不是 A 的 parent,a1 的声明类型。这也有例外,例如当开始对接口进行强制转换时。

Why is there a difference? Why doesn't the compiler see the problem in the first case?

编译器只验证基于静态类型(变量或表达式的类型)的类型转换。它不查看运行时 class,这当然要等到实际创建 object 的运行时才可用。当它可以确定转换不可能有效时(例如在第二种情况下),它将失败。在第一种情况下,从 AB 的转换通过了编译,因为声明的类型是兼容的(即 A object 可能是 [=12= 的实例],编译器将其留给运行时检查实际的 object)。在第二种情况下,编译器知道 A object 永远不可能是 String 的实例(因为 StringA 的类型层次结构中不存在, 这在运行时不会改变)

A 类型的变量可以分配 B 的实例,因为 B A。例如,狗是动物,所以标有“动物”的框可能包含一只狗。

但是类型 A 的变量不能被分配一个字符串。例如,标有“动物”的盒子不会包含积木。

您可能会问自己,当我们看到代码显然会失败时,为什么编译器不报错——变量不可能是 B;这是一个 A!

编译器在进行检查时只查看变量的类型。它不检查之前出现的代码。尽管您的示例很简单,但在一般情况下检查变量实际包含的内容将是一项不可能完成的任务。

class A 的 class 层次结构图为:

对象 -> A -> B(注意每个 class 扩展对象)

B b1 = (B) a1;

上面的行编译是因为 B 扩展了 A,因此编译器将其视为有效的向下转换。 java 编译器仅通过检查 B 的 class 层次结构(B 是直接还是间接扩展 A)来检查类型 A 的对象是否可能是类型 B。此时它不检查对象 A 的实际类型。它没有以这种方式实现,因为它会在编译器中增加很多复杂性。此外,如果一个对象正在被向下转换(可能调用某些特定的 sub class 方法),那么程序员就有责任了解该对象的特定类型。在此示例中,由于 a1 无法转换为类型 B,因此 JVM 将在运行时检测到它。

字符串 b1 = (字符串) a1;

在这种情况下,class字符串不在A的class层次结构图中。因此在编译时可以检测到这是一个无效的转换。