继承:是将代码从超类实际上“复制”到子类,还是“由子类”引用?


10

Sub是class的子类Sup。这实际上意味着什么?换句话说,“继承”的实际含义是什么?

选项1:将Sup中的代码虚拟复制到Sub。(如在“复制-粘贴”,但没有复制的代码视觉上在子类中看到的)。

示例:methodA()最初是Sup中的一种方法。Sub扩展了Sup,因此methodA()(实际上)被复制粘贴到Sub。现在Sub具有一个名为的方法methodA()methodA()在每一行代码中均与Sup相同,但完全属于Sub,并且不依赖于Sup或以任何方式与Sup相关。

选项2: Sup中的代码实际上没有复制到Sub。它仍然只在超类中。但是该代码可以通过子类访问,并且可以由子类使用。

示例:methodA()是Sup中的一种方法。Sub扩展了Sup,因此现在methodA()可以通过Sub进行访问,如下所示:subInstance.methodA()。但这实际上将methodA()在超类中调用。这意味着methodA()将在超类的上下文中运行,即使它是由子类调用的。

问题:这两个选项中的哪一个实际上是如何工作的?如果它们都不是,那么请描述这些事情实际上是如何工作的。


这很容易为您自己测试-编写代码,检查类文件(甚至校验和也可以),修改超类,再次编译,再次查看类文件。您可能还会发现阅读第3章。针对 JVM规范的Java虚拟机进行编译有助于理解(特别是3.7节)。

@MichaelT“ 虚拟复制”是关键字。同样,即使代码被按原样复制,也可能仅在类加载之后发生。

@delnan,很奇怪Hotspot(或其他运行时优化器)是否会在某个时候内联代码,但这成为了JVM的实现细节,可能会因一个JVM而异,因此无法正确回答。最好的办法是查看编译后的字节码(以及描述实际情况的invokespecial操作码用法)

Answers:


13

选项2。

字节码在运行时动态引用:这就是为什么发生LinkageErrors的原因。

例如,假设您编译了两个类:

public class Parent {
  public void doSomething(String x) { ... }
}

public class Child extends Parent {
  @Override
  public void doSomething(String x) {
    super.doSomething(x);
    ...
  }
}

现在修改并重新编译父类,而无需修改或重新编译子类

public class Parent {
  public void doSomething(Collection<?> x) { ... }
}

最后,运行使用子类的程序。您将收到一个NoSuchMethodError

如果应用程序尝试调用类的指定方法(静态或实例),并且该类不再具有该方法的定义,则抛出该异常。

通常,此错误是由编译器捕获的。如果类的定义发生了不兼容的更改,则只有在运行时才会发生此错误。


7

让我们从两个简单的类开始:

package com.michaelt.so.supers;

public class Sup {
    int methodA(int a, int b) {
        return a + b;
    }
}

然后

package com.michaelt.so.supers;

public class Sub extends Sup {
    @Override
    int methodA(int a, int b) {
        return super.methodA(a, b);
    }
}

编译方法A并查看一个字节码得到:

  methodA(II)I
   L0
    LINENUMBER 6 L0
    ALOAD 0
    ILOAD 1
    ILOAD 2
    INVOKESPECIAL com/michaelt/so/supers/Sup.methodA (II)I
    IRETURN
   L1
    LOCALVARIABLE this Lcom/michaelt/so/supers/Sub; L0 L1 0
    LOCALVARIABLE a I L0 L1 1
    LOCALVARIABLE b I L0 L1 2
    MAXSTACK = 3
    MAXLOCALS = 3

您可以在那里看到带有invokespecial方法的对象,它针对Sup类methodA()进行查找。

invokespecial操作码具有以下逻辑:

  • 如果C包含一个实例方法的声明,该实例方法的名称和描述符与解析的方法相同,则将调用此方法。查找过程终止。
  • 否则,如果C具有超类,则使用C的直接超类递归执行相同的查找过程。要调用的方法是此查找过程的递归调用的结果。
  • 否则,引发AbstractMethodError。

在这种情况下,没有实例名称和其类的描述符相同的实例方法,因此第一个项目符号不会触发。但是第二个要点是-有一个超类,它调用了超类的methodA。

编译器不会对此进行内联,并且该类中没有Sup源的副本。

但是故事还没有结束。这只是编译后的代码。一旦代码到达JVM, HotSpot就可以参与其中。

不幸的是,我对此并不太了解,因此我将在此问题上寻求权威,然后转到Java中的内联,据说HotSpot 可以内联方法(甚至是非最终方法)。

转到文档,请注意,如果某个特定的方法调用成为热点而不是每次都不进行查找,则可以内联此信息-有效地将代码从Sup methodA()复制到Sub methodA()。

这是在运行时在内存中完成的,具体取决于应用程序的行为方式以及加快性能所需的优化。

OpenJDK的HotSpot Internals中所述: “方法通常是内联的。静态,私有,最终和/或“特殊”调用很容易内联。”

如果您深入研究JVM的选项,则会发现一个选项-XX:MaxInlineSize=35(默认值为35),它是可以内联的最大字节数。我将指出这就是Java喜欢拥有许多小方法的原因-因为它们可以轻松地内联。这些较小的方法在被调用时会变得更快,因为它们可以被内联。而且虽然可以使用该数字并将其增大,但可能会导致其他优化效果降低。(相关的SO问题:HotSpot JIT内联策略,指出了许多其他选择,可以窥探HotSpot正在执行的内联内部)。

因此,不可以-编译时不内联代码。而且,是的-如果性能优化可以保证在运行时很好地内联代码。

我写的有关HotSpot内联的所有内容仅适用于Oracle分发的HotSpot JVM。如果您查看Wikipedia的Java虚拟机列表,不仅有HotSpot,而且这些JVM处理内联的方式可能与上文所述完全不同。Apache Harmony,Dalvik,ART-那里的情况可能有所不同。


0

该代码未复制,可通过引用进行访问:

  • 子类引用其方法,而超类
  • 超类引用其方法

编译器可以优化在内存中表示/执行的方式,但这基本上是结构

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.