Java多态的实现原理

多态及多态的表现

  • 在面向过程编程中,方法名是唯一的,调用方法即有唯一的行为,然而方法的调用并不等于方法的执行,然而OOP中引入的多态,可以让同一方法名有不同的的行为,比如说下图,都是调用“打印”这个指令,然而用不同的实体执行则会得到不同的效果。
    在这里插入图片描述
  • 总体来说,多态由以下两种表现
    • 重写:发生在父子类之间,子类可以重写父类方法,要保证方法名和参数列表相同
    • 重载:放生在同一个类里面,方法名相同而参数列表不同(无论是类型还是数量不同都算)
  • 难点:重写是比较好理解的,不管引用是父类还是子类,只要是父类不用向下强转也能找到的方法(其实就是重写),执行的都是实例的方法。然而当重载遇到重载遇到不同方法中的参数存在继承关系时,则变得有点复杂(如下例),本文就是希望总结出这个问题在JVM当中的解决思路。
// 输出到底是 hello, man  hello, woman 吗?
public class StaticDispacth {
    static abstract class Human {}

    static class Man extends Human {}

    static class Woman extends Human{}

    public static void sayHi() {
        System.out.println("sayHi");
    }

    public void sayHello(Human guy) {
        System.out.println("hello, guy");
    }

    public void sayHello(Man guy) {
        System.out.println("hello, man");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello, woman");
    }

    public static void main(String[] args) {
        sayHi();
        Human man = new Man();
        Human woman = new Woman();
        StaticDispacth sr = new StaticDispacth();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

实际上只会输出两个 hello, guy,而问题就是为什么传入Man和Woman的实例却调用了Human的方法呢?
在这里插入图片描述

从字节码指令探究

JVM中定义了5条方法调用字节码指令,分别可以归类到下面两种

  • 非虚方法:可以简单的理解成不可重写的方法都属于这类,指令有
    • invokestatic:调用静态方法
    • invokespecial:调用实例构造器方法、私有方法、父类方法
  • 虚方法:可以理解成能够重写的方法
    • invokevirtual:调用所有的虚方法,如实例方法(但是final方法不可被重写也分到了这一类)
    • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
    • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

invokespecial的执行

  • 对于方法,我们用“方法签名来描述”,比如 add(int a, int b)的签名是 add(int, int),而在重写引入的情况下,我的理解是应该由 调用实例(接受者).方法签名来确认应该使用哪个方法版本,因此重点就在于确定接受者和方法签名。
  • invokespecial执行的过程为
    1. 找到操作数栈顶的第一个元素所指向的对象的实际类型C
    2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;不通过则报异常
    3. 否则,按照继承关系从下往上一次对C的各个无泪进行第二部的搜索和权限验证
    4. 如果始终没有找到合适的方法,则跑出AbstractMethodError异常

静态分派与重载

  • 重载发生在一个类中,因此接受者是确定的,当遇到重载的多个方法中参数数量相同而参数类型有继承关系时,需要进行静态分派来找到合适的方法版本。
  • 对于“父类引用指向子类实例”,如上面的 Human man = new Man(); 这种情况,我们称Human为静态类型,而Man称为实际类型,重载时是通过参数的静态类型而不是实际类型作为判定依据的
  • 如上代码可见,由于man和woman的静态类型都是Human,因此调用的是参数为Human的方法。

动态分派与重写

  • 重写发生在父子类之间,参数列表是一摸一样的,关键就在于确定接收者(PS:如果出现同时重载重写的情况,会先在编译期就确定了方法签名)。
  • 在执行invokevirtual之前,会从局部变量表中a_load出接受者压到操作数栈顶(关于操作数栈等知识请看3),再调用相应的方法。
// 动态分配是多态——重写的体现
public class DynamicDispatch {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {

        @Override
        protected void sayHello() {
            System.out.println("man say Hello");
        }

    }

    static class Woman extends Human {

        @Override
        protected void sayHello() {
            System.out.println("woman say Hello");
        }
    }

    // PS:子类新增加的方法父类引用是看不见的,想要调用子类新增的方法需要向下转型
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();

    }
}
  • 执行结果
    在这里插入图片描述
  • 查看字节码
    在这里插入图片描述

单分派与多分派

  • 方法的接受者与方法的参数统称为方法的宗量,单分派是只方法版本由一个宗量决定,多分派相反

虚拟机动态分派的实现

  • 动态分派查找合适的方法版本是十分频繁的操作,而为了优化搜索,JVM采用类在方法区中建立虚方法表(vTable)的方式,使用虚方法表索引来代替元数据查找——就是索引的思想
  • 虚方法表中存放着各个方法的实际入口地址,需要遵循以下规则:
    1. 父、子类的虚方法表中都应当有一样的索引序号(子类对父类方法的访问权限会在调用时会检查)
    2. 如果某个方法在子类中没有被重写,子类的虚方法表中相应的方法入口地址和父类的相同
    3. 如果子类中重写了父类的方法,则子类的虚方法表对应位置指向重写后的方法

总结

  • 重载调用关注传入类型的静态类型
  • 重写调用关注接受者(即方法调用者)的实际类型
  • 为了满足动态分配频繁查找,使用“虚方法表”进行优化

参考资料

  1. 《深入理解Java虚拟机》——周志明
  2. 【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派
  3. 栈帧、局部变量表、操作数栈
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值