【问题标题】:Selection of the Correct Method for Invocation选择正确的调用方法
【发布时间】: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'...
  • 我实际上回答了这个问题,然后重新阅读了这个问题并变得同样困惑:)

标签: java methods


【解决方案1】:

方法的动态绑定在运行时适用于具有相同签名的不同实现之间,但所有可能方法的集合是在编译器查看签名时静态确定的。由于您将p 声明为Parent 类,因此在运行时它将查找Parent 类中存在的方法,并且如果存在更具体的实现(由于子类,例如在您的示例中) ,那么它将被选择而不是祖先。

由于您的方法没有覆盖任何内容,因此在编译时它将选择不同的签名(带有Object 的签名)并忽略它。由于属性p 的类型,它在运行时不会显示为匹配的可能性。

【讨论】:

  • 老实说,你回复的前两个字让我找到了正确的答案。我很快意识到,当我应该查看方法调用的运行时评估部分时,我正在咨询 JLS 的错误部分(专门针对方法调用的编译时问题的部分)。我在上面的问题中添加了完整的答案。感谢您的帮助。
【解决方案2】:

您的答案实际上很简单:您对方法的更改破坏了您对继承的使用。

Parent 只有一个 doIt 方法,它带有一个 Object 参数。当您在父级上调用 doIt("test") 时,它会查看它是否在子级中被覆盖。由于 doIt(Object s) 在子级中没有被覆盖,因此使用了父级方法。即使您传递了一个字符串,但仍会调用父方法,因为 doIt(Object s) 与 doIt(String s) 不同。

简单地说,当你改变签名时,你并没有重写方法,而是重载了它。

【讨论】:

  • 它实际上并没有“破坏我对继承的使用”——它不再让我的方法覆盖父类中的方法,但继承仍然完全有效。
【解决方案3】:

要调用的方法签名是在编译时确定的,因此 p.doIt("test") 将在运行时调用相应类的 doIt(Object o) 方法。 doIt(String s) 甚至没有被查看。想象一下,在编写 Poly makeItHappen 时 Child.java 不存在——您还必须将 Child 构造函数抽象到不同类的工厂方法中才能编译。您可以重新实现工厂方法和 Child.java,而无需重新编译 Poly.java。这允许您对基类提供的接口和相对高效的函数调用进行编程。您的 Poly makeItHappen 只需要知道 doIt(Object o) 存在。

如果你认为继承的可能实现是通过一个虚函数表,那么 doIt(String s) 和 doIt(Object o) 有不同的表条目。 Parent 只有一个 doIt(Object o) 的条目。 Child 具有与 Parent 相同的 doIt(Object o) 条目和它自己的 doIt(String s) 条目。在编译时,要调用的方法是 doIt(Object o) 槽中的方法。

【讨论】:

    【解决方案4】:

    您将Child 向上转换为Parent

    Parent p = new Child();
    

    当您调用p.doIt("test"); 时,您将获得Child 行为的唯一方法是通过覆盖的方法。

    由于您将Child 中的方法更改为doIt(String s),它不再覆盖Parent 中的任何内容,因此doIt(Object o)Parent 中被调用。

    【讨论】:

      【解决方案5】:

      嗯,试图解决“为什么”运行时不玩捉迷藏的问题,并使用整个类定义寻找与编译器要求的方法相比“更好”的方法匹配,举个例子。显然,这个 API 一开始就很糟糕,但是您可以想象如果方法签名很长,这种情况可能会意外发生。

      /**
      * Vendor API you program to
      */
      public class IPv4Manager {
        public void terminateSocket(Object obj) {
          //terminate IPv4 socket
        }
      }
      
      /**
      * Vendor class that's injected at runtime that you have no knowledge of 
      * and do not compile against.
      */
      public class IPv4And6Manager extends IPv4Manager {
      
        public void terminateSocket(Byte[] packet) {
          //terminate IPv6 socket
        }
      
      }
      
      /**
      *Your user code
      */
      public void terminateIPv4Socket(Byte[] packet) {
        IPv4Manager manager = managerFactory.getV4Manager(); //returns you an instance of 4And6Manager
        manager.terminateSocket(packet);
      }
      

      您真的希望运行时尝试比您更聪明并调用比编译器要求的方法“更好”的方法吗?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-12
        • 1970-01-01
        • 1970-01-01
        • 2020-02-13
        相关资源
        最近更新 更多