方法的 静态分派 与 动态分派

154 阅读3分钟

方法的静态分派

Grandpa g1 = new Father();

以上代码,g1 的静态类型是 Grandpa,而 g1 的实际类型(真正指向的类型)是 Father

我们可以得到这样一个结论,变量的静态类型是不会发生变化的,而实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定

public class MyTest4 {
     //方法重载,是一种静态的行为, 在执行方法重载的时候,编译期就可以完全确定
     // jvm **唯一判断的依据** 就是方法本身接收的参数的静态类型
    public void test(Grandpa grandpa) {
        System.out.println("Grandpa");
    }

    public void test(Father father) {
        System.out.println("father");
    }

    public void test(Son son) {
        System.out.println("Son");
    }

    public static void main(String[] args) {
        MyTest4 MyTest4 = new MyTest4();
        Grandpa g1 = new Father();
        Grandpa g2 = new Son();
        MyTest4.test(g1);//Grandpa
        MyTest4.test(g2);//Grandpa
    }
}

class Grandpa {
}

class Father extends Grandpa {
}

class Son extends Father {
}

方法的动态分派

方法的动态分配涉及到一个重要的概念:方法接受者(这个方法到底是由那个对象调用的)

invokevirtual 字节码指令的多态查找流程

  1. 找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为 X
  2. 如果在类型 X 中找到与常量池中描述符和名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束
  3. 如果没有找到,就沿着这个流程找其父类
  4. 如果始终没有找到合适的方法,则抛出抽象方法错误的异常 从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。

从上面过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为方法接受者)的实际类型,所以当调用 invokevirtual 指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。

比较方法重载与方法重写,我们可以得到结论方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为

针对方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable);(虚方法表存放着每个方法实际的真正入口调用地址,虚方法表是在连接阶段完成初始化,子类继承父类的方法且没有重写该方法,子类的虚方法表会指向父类虚表的入口地址,若是重写了,子类的虚表索引与父类的一致)

针对 invokeInterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(Interface method table);

public class MyTest5 {
    public static void main(String[] args) {
        Fruit apple = new Apple();
        Fruit orange = new Orange();
        apple.test();  //<com/poplar/bytecode/Fruit.test>将符号引用转换为直接引用
        orange.test();
        apple = new Orange();
        apple.test();
    }
}

class Fruit {

    public void test() {
        System.out.println("Fruit");
    }
}

class Apple extends Fruit {

    @Override
    public void test() {
        System.out.println("Apple");
    }
}

class Orange extends Fruit {
    @Override
    public void test() {
        System.out.println("Orange");
    }
}

反编译 main 函数 的 code

17、20 相同的符号引用指向不同的直接引用

 0 new #2 <pluto/bytecode/Apple>
 3 dup
 4 invokespecial #3 <pluto/bytecode/Apple.<init>>
 7 astore_1
 8 new #4 <pluto/bytecode/Orange>
11 dup
12 invokespecial #5 <pluto/bytecode/Orange.<init>>
15 astore_2
16 aload_1
17 invokevirtual #6 <pluto/bytecode/Fruit.test>
20 aload_2
21 invokevirtual #6 <pluto/bytecode/Fruit.test>
24 new #4 <pluto/bytecode/Orange>
27 dup
28 invokespecial #5 <pluto/bytecode/Orange.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <pluto/bytecode/Fruit.test>
36 return