方法调用——分派(二)

Java具备面向对象的三大特征:封装、继承、多态。分派过程将会揭示多态特征的一些最基本体现

一、什么是动态分派

动态分派和多态的另一个重要体现——重写(Override)有着密切的关联。我们把运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

二、方法动态分派

//方法动态分派演示
public class DynamicDispatch {
    static abstract class Human{
        protected abstract void say();
    }

    static class Man extends Human{
        @Override
        protected void say() {
            System.out.println("hello man");
        }
    }

    static class Woman extends Human{
        @Override
        protected void say() {
            System.out.println("hello woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();

        man.say();
        woman.say();

        man = new Woman();
        man.say();
    }
}
运行结果

对于熟悉面向对象思想的程序员,这个运行结果是理所当然的。但对于虚拟机来说,是如何知道要调用哪个方法的呢?

显然,这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量在调用say()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因是这两个变量的实际类型不同。

下面,我们使用javap命令输出这段代码的字节码

常量池,注意第6项
字节码

首先我们来看0~15行,0~15行是准备动作,作用是建立man和woman的内存空间、调用Man和Woman类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表的Slot中,对应代码中的:

        Human man = new Man();
        Human woman = new Woman();

接下来的6~21行是关键部分,16和20行代表把创建的对象的引用压倒栈顶,这两个对象是将要执行say()方法的所有者,称为接收者(Receiver);17和21行是方法调用指令,对比常量池,可以看到无论是指令(invokevirtual)还是参数(常量池中的第6项)都是一致的,但这两句指令最终执行的目标方法并不相同。

导致这种情况的原因在于 invokevirtual 指令的多态查找, invokevirtual 指令的运行时解析过程大致分为以下几个步骤

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

由于 invokevirtual 指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的 invokevirtual 指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java中方法重写的本质。

发表评论

邮箱地址不会被公开。 必填项已用*标注