美文网首页
Java多态实现的关键

Java多态实现的关键

作者: 知止9528 | 来源:发表于2019-01-15 00:02 被阅读7次

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

    这里就涉及到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的字节码可以看到

    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;
    

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

    执行引擎(不考虑栈顶缓存).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()方法。

    相关文章

      网友评论

          本文标题:Java多态实现的关键

          本文链接:https://www.haomeiwen.com/subject/wnmsdqtx.html