java继承实现

tag5nh1u  于 2021-07-09  发布在  Java
关注(0)|答案(2)|浏览(374)

让我们考虑一下简单的接口:

interface Simple{
     void doSth();
}

以及实现它的两个类:

class A implements Simple{

     void someOtherMethod(){ .... }

     void doSth(){ ... }

     private void doSth(int x){ ... }
}

class B implements Simple{

     void methodA(){ ..}
     // many other methods

     void doSth(){ ... }

     private void doSth(Object o, long y){ ... }
}

现在,我可以很容易地写:

Simple s = new A();
s.doSth();

java的多态性将完成剩下的工作。考虑到实现类中可以定义更多的方法,甚至它们的返回类型可以是原始方法的子类,有人知道hotspot如何确保链接器链接到正确的方法吗?java是否确保接口方法总是从vtable中的某个偏移量开始,例如0?

vhipe2zx

vhipe2zx1#

interface InterfaceA{void method();}
class ClassA implements InterfaceA{void method(){}}
void methodA(InterfaceA[]o){
    for(int i=0;i<o.length;++i)o[i].method();
}
void methodB(ClassA[]o){
    for(int i=0;i<o.length;++i)o[i].method();
}

所以调用methoda要比调用methodb慢得多

yhuiod9q

yhuiod9q2#

在我们研究这一点之前,让我们简化一下示例:

interface Foo {
    void bar();
}

class AFoo implements Foo {
    int i;

    @Override
    public void bar() {
        i++;
    }
}

class AnotherFoo implements Foo {
    int i;

    @Override
    public void bar() {
        i--;
    }
}

public class Test {
    public static void main(String[] args) {
        Foo foo = new AFoo();
        foo.bar();
    }
}

编译后,我们使用
javap.exe-详细测试.class
要检查生成的字节码:

public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #16                 // class tools/AFoo
         3: dup
         4: invokespecial #18                 // Method tools/AFoo."<init>":()V
         7: astore_1
         8: aload_1
         9: invokeinterface #19,  1           // InterfaceMethod tools/Foo.bar:()V
        14: return

在类加载时,代码将被链接,这是由java语言规范指定的,如下所示:
类或接口的二进制表示使用二进制名称象征性地引用其他类和接口及其字段、方法和构造函数(§13.1)其他类和接口(§13.1). 对于字段和方法,这些符号引用包括字段或方法所属的类或接口类型的名称、字段或方法本身的名称以及适当的类型信息。
在可以使用符号引用之前,必须对其进行解析,其中检查符号引用是否正确,并且通常用直接引用替换,如果重复使用该引用,则可以更有效地处理该直接引用。
注意,这个“直接引用”是指方法的声明。如果有多个实现,运行时此时无法知道将使用哪个方法。也就是说,多态性不是在java语言规范调用链接的过程中解决的,而是在执行实际的方法调用表达式时解决的。这由java虚拟机规范指定:
设c是objectref的类。通过以下查找过程选择要调用的实际方法:
如果c包含一个示例方法的声明,该示例方法的名称和描述符与解析的方法相同,那么这就是要调用的方法,并且查找过程终止。
否则,如果c有一个超类,则使用c的直接超类递归地执行相同的查找过程;要调用的方法是递归调用此查找过程的结果。
否则,将引发abstractmethoderror。
如何真正实现这一点取决于jvm的实现。对于oracle hotspot jvm,文档包含了相当详细的解释:
链接invokeinterface调用时,链接器将调用解析为接口中的抽象目标方法。这可以归结为一个目标接口和该接口中所谓的itable索引。
jvm验证器从不静态地保证目标接口;每个invokeinterface接收器都被类型化为一个简单的对象引用。因此(与invokevirtual调用不同),不能对接收器的vtable布局进行任何假设。相反,必须更仔细地检查接收方的类(由它的klass字段表示)。如果虚拟调用可以盲目地执行两个或三个间接操作以到达目标方法,则接口调用必须首先检查接收方的类,以确定(a)该类是否实际实现了该接口,以及(b)如果是,该接口的方法在该特定类中的记录位置。
在实现接口的每个类中,接口的方法都以固定的偏移量显示,没有简单的前缀方案。相反,在一般(非单态)情况下,程序集编码的存根例程必须从接收方的instanceklass获取一个已实现接口的列表,并遍历该列表以查找当前的目标接口。
一旦找到了接口(在接收方的instanceklass中),事情就变得简单了,因为接口的方法被安排在一个itable或“interface method table”中,一个方法的显示,对于实现所讨论的接口的每个类,其时隙结构都是相同的。因此,一旦在接收方的instanceklass中找到接口,关联的偏移量就会将程序集存根定向到instanceklass中嵌入的itable(正如人们所期望的,就在vtable之后)。此时,调用就像虚拟方法调用一样进行。
接口调用的优化与虚拟调用的优化几乎相同。与虚拟调用一样,大多数接口调用都是单态的,因此可以通过廉价的检查将其呈现为直接调用。
以下是多态接口调用的通用指令跟踪:

callSite:
    set #calledInterface, CHECK
    call #itableStub[itableSlot]
---
itableStub[itableSlot]:
    load (RCVR + #klass), KLASS_TEM
    load (KLASS_TEM + #vtableSize), TEM
    add  (KLASS_TEM + TEM), SCAN_TEM
tryAgain:
        # this part is repeated zero or more times, usually zero
    load (SCAN_TEM + #itableEntry.interface), TEM
    cmp TEM, CHECK
    jump,eq foundInterface
    test TEM
    jump,z noSuchInterface
    inc #sizeof(itableEntry), SCAN_TEM
    jump tryAgain
tryAgain:
    load (SCAN_TEM + #itableEntry.interface), TEM
    cmp TEM, CHECK
    jump,eq foundInterface
foundInterface:
    load (SCAN_TEM + #itableEntry.offset), TEM
    load (KLASS_TEM + TEM + #itableSlot), METHOD
    load (METHOD + #compiledEntry), TEM
    jump TEM
---
compiledEntry:
    ...

总共有六个内存引用和两个非局部跳转。
迂腐的注解:以上所有内容都适用于调用接口方法。调用类中声明的抽象方法使用不同的字节码指令,并且在oracle hotspot jvm中使用稍微简单的实现。

相关问题