【问题标题】:Runtime polymorphism decision运行时多态性决策
【发布时间】:2026-01-22 19:30:01
【问题描述】:

我在某处读到运行时多态性是语言动态类型的结果。通过检查下面的代码,我们可以看到运行时多态性的一个清晰示例。

class A{
    do(){}
}
class B extends A{
    do(){}
}
...
A ex = new B();
ex.do();

由于存在超类类型引用,编译器无法决定哪个类型将实际引用引用并在运行时绑定方法。但是下面使用相同类定义的用法呢?

我的第一个问题是针对下面的示例;

class A{
    do(){}
}
class B extends A{
  //no overriding
}
...
A ex = new B();
ex.do();

层次结构中只有一个版本的方法 do()。系统是否仍然等待运行时绑定方法?还是在编译时绑定?

我的第二个问题是针对下面的示例;

class A{
    do(){}
}
class B extends A{
  do(){}
}
...
B ex = new B();
ex.do();

现在有子类(继承链中最低)类型引用。会在运行时绑定吗?

【问题讨论】:

  • 在编译时很难确定有多少实现可用,因为有人可以在运行时添加它自己的实现。但是在运行时 JIT 可以推测层次结构并做以下事情*.com/a/33351238/1352098

标签: java jvm polymorphism compiler-optimization


【解决方案1】:

在编译非静态方法的调用时,javac 将始终使用 invokevirtual 指令,因此在编译时不会进行优化。

但由于方法调用的去虚拟化是一项重要的优化(保存 vtable 查找,可能内联方法),运行时(热点等)将尝试在可能的情况下应用它,因为代码分析.

所以在您的第二个示例(第三个代码块)中,运行时可能认识到它可以用对B.do 的调用替换对A.do 的虚拟调用,因为ex 实际上是一个B(在这种情况下,运行时应该很容易弄清楚)。

对于您的第一个示例(第二个代码块),还有另一种优化技术。运行时首先看到类A。任何A.do 的调用现在都编译为静态调用,就好像不存在覆盖A.do 的派生类一样。如果稍后加载这样的类,运行时将回滚这个乐观假设并引入虚拟方法调用。

【讨论】:

  • 对第三个例子有什么见解吗?
  • @ErickG.Hagstrom 澄清了参考文献:“第二个示例”=问题中的第三个代码块
【解决方案2】:

首先,术语“动态类型”,它在您的问题中的使用方式,充其量是误导。 Java 不是一种“动态类型的编程语言”。它提供了某些动态类型检查,例如类型转换和 instanceof 运算符,但在您的代码示例中,不涉及动态类型检查。都是静态类型的。

顺便说一句,do 在 Java 中不是合法的方法名称。但是假设A声明了一个方法doSomething并且有一个子类B,那么B是否覆盖doSomething与Java编译器完全无关(除非它改变了访问修饰符)。

关键是,AB 是不同的类,可以独立(重新)编译,并且不能保证 B 在运行时仍然(不)覆盖该方法。但规范认为此类更改在不破坏二进制兼容性的合法范围内:

13.4.24. Method Overriding

如果将实例方法添加到子类并覆盖超​​类中的方法,则子类方法将通过预先存在的二进制文件中的方法调用找到,并且这些二进制文件不受影响。

如果将类方法添加到类中,则除非引用的限定类型是子类类型,否则将找不到该方法。

注意关于“类方法”的最后一句话,又名static 方法。这意味着当A 声明static 方法m 并通过B.m 调用它时,如果B 碰巧声明了这样的方法,则调用可能会在static 声明的static 方法中结束在运行时,即使在编译时看到的版本没有。因此,即使是早期绑定的非多态方法最终也会在运行时解析,并且可能会找到与编译时不同的目标。与后期绑定方法的区别在于,一旦解决了早期绑定方法,调用总是被分派给该方法,而不依赖于在运行时可能更改的任何属性。

对于可覆盖的方法,该方法是根据调用它的引用的编译时类型来解析的,那么在引用的实际运行时类型中可能存在覆盖方法.这是向前回答潜在后续问题的地方:

13.4.17. final Methods

将声明为 final 的方法更改为不再声明为 final 不会破坏与预先存在的二进制文件的兼容性。

换句话说,当您在编译时调用 final 的方法时,编译器不会利用目标方法是 final 的事实,因为该方法可能不是 @ 987654346@ 在运行时,这种可能性不得破坏兼容性。

唯一得到特殊处理的方法调用是private 方法的调用。由于private 方法的调用者总是与方法声明本身在同一个类中,所以它们总是一起编译,不受独立演化的影响。

【讨论】:

  • 对不起,我的困惑。它起源于 Grady Booch 的 OOAD 书。下面的截图显示了源代码。感谢您的提醒。 imgur.com/9iirXRj
【解决方案3】:

编译器在编译时绑定还是 JVM 在运行时绑定是故意未定义的。 Java 规范不会以一种或另一种方式说明 - 它只会说明在代码运行时将达到预期的结果[需要引用]。

早/晚绑定是一种优化,因此是可选的。

【讨论】: