【发布时间】:2011-04-28 17:54:27
【问题描述】:
取以下代码:
public class Parent
{
public String doIt(Object o) {
return "parent";
}
}
public class Child extends Parent
{
public String doIt(Object s) {
return super.doIt(s) + ": " + "child";
}
}
public class Poly
{
public String makeItHappen() {
Parent p = new Child();
return p.doIt("test");
}
}
由于 Child.doIt() 覆盖 Parent.doIt(),调用 Poly.makeItHappen() 会导致将其打印到控制台:
parent: child
但是,如果我将 Child.doIt() 更改为如下所示:
public String doIt(String s) {
return super.doIt(s) + ": " + "child";
}
现在 Child.doIt() 不会覆盖 Parent.doIt()。当你调用 Poly.makeItHappen() 时,你会得到这个结果:
parent
我对此有点困惑。 p 的编译时类型是 Parent 所以我当然理解它发现 Parent.doIt() 作为一种可能适用的方法,但是,鉴于 p 的运行时类型是 Child,我不确定为什么 Child.doIt( ) 不是。假设它们都被确定为可能适用的方法,我希望 Child.doIt(String) 会在 Parent.doIt(Object) 上被调用,因为它更具体。
我已经尝试咨询JLS 并发现了这一点:
15.12.1 编译时步骤 1:确定要搜索的类或接口
...
在所有其他情况下,限定名称的格式为 FieldName 。标识符;那么方法的名称是标识符,并且要搜索的类或接口是由 FieldName 命名的字段的声明类型 T,如果 T 是类或接口类型,或者如果 T 是类型变量,则为 T 的上限.
对我来说,这表示将使用 p 的编译时类型。当我看到 Parent.doIt(Object) 被调用而 Child.doIt(String) 没有被调用时,这是有道理的。但就 Child.doIt() 正确覆盖 Parent.doIt() 时注意到的多态行为而言,这没有任何意义 - 在这种情况下,正在分析 Parent 和 Child 的两种方法以找到可能适用的方法,所以为什么不在第二种情况下,也是?
我知道我在这里遗漏了一些东西,但我无法弄清楚为什么我会看到我的行为。如果有人能对此有所了解,我将不胜感激。
编辑:找到答案:
感谢 Jack 的回复,我能够在 JLS 中找到答案。我之前提到的 JLS 部分实际上侧重于在编译时找到正确的方法,并没有涵盖在运行时调用正确方法的过程。可以在 JLS 中标题为 15.12.4 Runtime Evaluation of Method Invocation 的部分中找到该过程的这一部分。
在那里,我发现了这段文字:
否则调用方式为interface、virtual、super,可能会发生覆盖。使用动态方法查找。动态查找过程从一个类S开始,确定如下:
我的调用方式是虚拟的,所以上面的说法适用...
如果调用方式是接口或虚拟,则S最初是目标对象的实际运行时类R。
...
动态方法查找使用以下过程搜索类 S,然后根据需要搜索类 S 的超类以查找方法 m。
好的,所以这对我来说似乎很奇怪。据此,JVM 将开始在 Child 中寻找一个适用的方法来调用,这会让我相信 Child.doIt(String) 会被调用。但是,继续阅读……
令 X 为方法调用的目标引用的编译时类型。
...
如果类 S 包含一个名为 m 的非抽象方法的声明,该方法具有在编译时确定的方法调用所需的相同描述符(相同数量的参数、相同的参数类型和相同的返回类型)(§ 15.12.3),然后:
类“S”,即 Child,确实包含一个方法,其描述符与编译时确定的方法调用相同(毕竟,字符串“是一个”对象,所以描述符是相同的)。似乎仍然应该调用 Child.doIt(String),但请继续阅读...
如果调用方式是虚的,S中的声明覆盖(§8.4.8.1)X.m,那么S中声明的方法就是要调用的方法,过程终止。
...
否则,如果 S 具有超类,则使用 S 的直接超类代替 S 递归执行相同的查找过程;要调用的方法是此查找过程的递归调用的结果。
粗体部分是其中非常重要的部分。正如我所提到的,当我更改 Child.doIt() 方法时,它不再覆盖 Parent 的 doIt() 方法。因此,即使 JVM 正在评估 Child.doIt() 方法作为调用的潜在候选者,它也无法被调用,因为它没有覆盖 X 中定义的方法,即 Parent。我真的被挂断了,因为我认为 JVM 甚至没有检查 Child.doIt 作为一种可能适用的方法,而且这似乎不正确。现在我相信 JVM 正在将该方法检查为可能适用的方法,但随后忽略它,因为它没有正确覆盖父方法。在这种情况下,我认为子类中的方法会被调用,不是因为它覆盖了父类方法,而是因为它是最具体的。然而,在这种情况下,情况并非如此。
JLS 中的下一行简单地解释了这个过程在超类上递归执行,导致 Parent.doIt(Object) 被调用。
直观地说,这对我来说完全有道理,但我无法理解 JVM 是如何实际执行这个过程的。当然,查看 JLS 的正确部分会有很大帮助。
【问题讨论】:
-
当我读到它时,我没有看到“do it”我看到了“dolt”:-X just sayin'...
-
我实际上回答了这个问题,然后重新阅读了这个问题并变得同样困惑:)