Java多态实现的关键

这里也是扩展篇之动态代理里面的内容,也是单独把它拿出来了,详细的可以去看扩展篇之动态代理。

这里就涉及到java的多态,多态是什么呢?
允许不同类的对象对同一消息做出响应,即根据发送对象的不同而采用多种不同的行为方式。

实现多态的技术称为动态绑定,指的是在运行期间判断对象所引用对象的实际类型,根据实际类型调用其相应的方法。另外一种就是在编译期进行绑定,也就是我们所说的静态绑定。

JVM实现晚期绑定的机制是基于virtual table,即虚方法表。JVM通过虚方法表在运行期动态的确定所调用目标类的目标方法。

在JVM加载Java类的过程中,JVM会动态的解析Java类的方法及其对父类方法的重写,进而构建出一个vtable.

java类在运行期进行动态绑定的方法,一定会被声明为public或者protected,并且没有static和final修饰.

原因是,如果java方法已经被static修饰,就根本不会参与到整个java的继承体系中。即动态绑定,其实可以理解为Java类实例与java方法搭配。静态方法根本不需要经过类实例。

java方法被private修饰,外部无法调用,因此也不会参与到继承体系。

方法被final修饰,子类无法重写,自然也不会出现多态。

若子类重写了父类的方法,则JVM会更新父类虚方法表中指向父类被重写方法的指针,让其指向子类中该方法的内存地址。如果子类中方法不是对父类方法的重写,JVM会向子类的虚方法表中插入一个新的指针元素,让其指向该方法的内存位置。

Java的字节码指令中方法的调用实现分为4种指令
Invokevirtual,包含了virtual dispatch(虚方法分发)机制。
Invokespecial,调用private和构造方法,绕过了虚方法分发
Invokeinterface,实现与invokevirtual类似。
Invokestatic,调用静态方法。

我们着重写下invokevirtual
虚拟机在执行invokevirtual指令的过程中,最终会读取被调用的类的虚方法表,并据此决定真是的目标调用方法。

我们可以验证一下

public class Animal {
    public  void say(){
        System.out.println("I am animal");
    }

    public static void main(String[] args) {
        Animal animal = new Dog();
        run(animal);

        animal = new Animal();
        run(animal);
    }
    public static  void run(Animal animal){
        animal.say();
    }
    static class  Dog extends  Animal{
        @Override
        public void say() {
            System.out.println("I am a dog");
        }
    }
}

输出结果为
I am a dog
I am animal

我们查看Animal.class的字节码可以看到

15854876-e208f4c25e74c05c.png
java多态验证.png
public static void run(com.yjm.Animal);
    descriptor: (Lcom/yjm/Animal;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #10                 // Method say:()V
         4: return
      LineNumberTable:
        line 19: 0
        line 20: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0 animal   Lcom/yjm/Animal;

为了便于理解字节码,可以看之前自己画的一个图,当然是在不考虑栈顶缓存的情况下,栈顶缓存主要是为了执行效率优化,比较复杂,所以画的是不考虑栈顶缓存的情况。

15854876-9d33d8218082e195.png
执行引擎(不考虑栈顶缓存).png

我们继续我们的animal字节码指令
aload_0表示从第0个slot位置加载java引用对象,由于Animal.run(Animal)方法是一个static静态方法,其入参并没有隐式的this指针,所以slot中的第一个局部变量就是Animal.run(Animal)的第一个入参的animal引用对象。接着就是第二步执行invokevirtual指令,invokevirtual指令后面的操作数是常量池的索引值,所代表的字符串是Method say:()V,代表运行期调用的是void say()。

在运行期,jvm将首先确定被调用的方法所属的java类的实例对象,jvm会读取被调用方法的堆栈,并获取堆栈中的局部变量表中的第0个slot位置的数据,该数据指向的是被调用方法所属的java类实例。

获取到类实例之后,便能通过对象获取到其对应的虚方法分发表。

当animal引用变量指向new Dog()对象的实例时,jvm会遍历Dog类锁对应的虚方法表,并搜搜其中名称为"say",签名为"void ()" 的方法,Dog类中存在该方法,jvm最终执行的就是Dog类的say()方法。同理,当animal 引用变量指向new Animal() 对象实例时,jvm最终执行的就是Animal类中的say()方法。

猜你喜欢

转载自blog.csdn.net/weixin_34102807/article/details/87550331