• override的实现原理


    转载 http://blog.csdn.net/fan2012huan/article/details/51007517

    基于基类的调用和基于接口的调用,从性能上来讲,基于基类的调用性能更高 。因为invokevirtual是基于偏移量的方式来查找方法的,而invokeinterface是基于搜索的。

    概述

    多态是面向对象程序设计的重要特性。多态允许基类的引用指向派生类的对象,而在具体访问时实现方法的动态绑定。
    java对方法动态绑定的实现方法主要基于方法表,但是这里分两种调用方式invokevirtual和invokeinterface,即类引用调用和接口引用调用。类引用调用只需要修改方法表的指针就可以实现动态绑定(具有相同签名的方法,在父类、子类的方法表中具有相同的索引号),而接口引用调用需要扫描整个方法表才能实现动态绑定(因为,一个类可以实现多个接口,另外一个类可能只实现一个接口,无法具有相同的索引号。这句如果没有看懂,继续往下看,会有例子。写到这里,感觉自己看书时,有的时候也会不理解,看不懂,思考一段时间,还是不明白,做个标记,继续阅读吧,然后回头再看,可能就豁然开朗。)。
    类引用调用的大致过程为:java编译器将java源代码编译成class文件,在编译过程中,会根据静态类型将调用的符号引用写到class文件中。在执行时,JVM根据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量,然后根据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,如果在实际类型的方法表中找到该方法,则直接调用,否则,按照继承关系从下往上搜索。
    下面对上面的描述做具体的分析讨论。

    JVM的运行时结构

    这里写图片描述
    从上图可以看出,当程序运行时,需要某个类时,类载入子系统会将相应的class文件载入到JVM中,并在内部建立该类的类型信息,这个类型信息其实就是class文件在JVM中存储的一种数据结构,他包含着java类定义的所有信息,包括方法代码,类变量、成员变量、以及本博文要重点讨论的方法表。这个类型信息就存储在方法区。
    注意,这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以是各个线程共享的内存区域),而在堆中可以有多个该class对象。可以通过堆中的class对象访问到方法区中类型信息。就像在java反射机制那样,通过class对象可以访问到该类的所有信息一样。
    方法表是实现动态调用的核心。方法表存放在方法区中的类型信息中。方法表中存放有该类定义的所有方法及指向方法代码的指针。这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。

    类引用调用invokevirtual

    代码如下:

    package org.fan.learn.methodTable;
    
    /**
     * Created by fan on 2016/3/30.
     */
    public class ClassReference {
        static class Person {
            @Override
            public String toString(){
                return "I'm a person.";
            }
            public void eat(){
                System.out.println("Person eat");
            }
            public void speak(){
                System.out.println("Person speak");
            }
    
        }
    
        static class Boy extends Person{
            @Override
            public String toString(){
                return "I'm a boy";
            }
            @Override
            public void speak(){
                System.out.println("Boy speak");
            }
            public void fight(){
                System.out.println("Boy fight");
            }
        }
    
        static class Girl extends Person{
            @Override
            public String toString(){
                return "I'm a girl";
            }
            @Override
            public void speak(){
                System.out.println("Girl speak");
            }
            public void sing(){
                System.out.println("Girl sing");
            }
        }
    
        public static void main(String[] args) {
            Person boy = new Boy();
            Person girl = new Girl();
            System.out.println(boy);
            boy.eat();
            boy.speak();
            //boy.fight();
            System.out.println(girl);
            girl.eat();
            girl.speak();
            //girl.sing();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    注意,boy.fight();girl.sing(); 这两个是有问题的,在IDEA中会提示“Cannot resolve method ‘fight()’”。因为,方法的调用是有静态类型检查的,而boy和girl的静态类型都是Person类型的,在Person中没有fight方法和sing方法。因此,会报错。
    执行结果如下:
    这里写图片描述
    从上图可以看到,boy.eat()girl.eat() 调用产生的输出都是”Person eat”,因为Boy和Girl中没有override 父类的eat方法。
    字节码指令:

    public static void main(java.lang.String[]);
      Code:
       Stack=2, Locals=3, Args_size=1
       0:   new     #2; //class ClassReference$Boy
       3:   dup
       4:   invokespecial   #3; //Method ClassReference$Boy."<init>":()V
       7:   astore_1
       8:   new     #4; //class ClassReference$Girl
       11:  dup
       12:  invokespecial   #5; //Method ClassReference$Girl."<init>":()V
       15:  astore_2
       16:  getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
       19:  aload_1
       20:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       23:  aload_1
       24:  invokevirtual   #8; //Method ClassReference$Person.eat:()V
       27:  aload_1
       28:  invokevirtual   #9; //Method ClassReference$Person.speak:()V
       31:  getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
       34:  aload_2
       35:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       38:  aload_2
       39:  invokevirtual   #8; //Method ClassReference$Person.eat:()V
       42:  aload_2
       43:  invokevirtual   #9; //Method ClassReference$Person.speak:()V
       46:  return
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    其中所有的invokevirtual调用的都是Person类中的方法。

    下面看看java对象的内存模型:
    这里写图片描述
    从上图可以清楚地看到调用方法的指针指向。而且可以看出相同签名的方法在方法表中的偏移量是一样的。这个偏移量只是说Boy方法表中的继承自Object类的方法、继承自Person类的方法的偏移量与Person类中的相同方法的偏移量是一样的,与Girl是没有任何关系的。

    下面再看看调用过程,以girl.speak() 方法的调用为例。在我的字节码中,这条指令对应43: invokevirtual #9; //Method ClassReference$Person.speak:()V ,为了便于使用IBM的图,这里采用跟IBM一致的符号引用:invokevirtual #12; 。调用过程图如下所示:
    这里写图片描述
    (1)在常量池中找到方法调用的符号引用
    (2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
    (3)根据this指针确定方法接收者(girl)的实际类型
    (4)根据对象的实际类型得到该实际类型对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用;如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。

    接口引用调用invokeinterface

    代码如下:

    package org.fan.learn.methodTable;
    
    /**
     * Created by fan on 2016/3/29.
     */
    public class InterfaceReference {
        interface IDance {
            void dance();
        }
    
        static class Person {
            @Override
            public String toString() {
                return "I'm a person";
            }
            public void speak() {
                System.out.println("Person speak");
            }
            public void eat() {
                System.out.println("Person eat");
            }
        }
    
        static class Dancer extends Person implements IDance {
            @Override
            public String toString() {
                return "I'm a Dancer";
            }
            @Override
            public void speak() {
                System.out.println("Dancer speak");
            }
            public void dance() {
                System.out.println("Dancer dance");
            }
        }
    
        static class Snake implements IDance {
            @Override
            public String toString() {
                return "I'm a Snake";
            }
            public void dance() {
                System.out.println("Snake dance");
            }
        }
    
        public static void main(String[] args) {
            IDance dancer = new Dancer();
            System.out.println(dancer);
            dancer.dance();
            //dancer.speak();
            //dancer.eat();
            IDance snake = new Snake();
            System.out.println(snake);
            snake.dance();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    上面的代码中dancer.speak(); dancer.eat(); 这两句同样不能调用。
    执行结果如下所示:
    这里写图片描述
    其字节码指令如下所示:

    public static void main(java.lang.String[]);
      Code:
       Stack=2, Locals=3, Args_size=1
       0:   new     #2; //class InterfaceReference$Dancer
       3:   dup
       4:   invokespecial   #3; //Method InterfaceReference$Dancer."<init>":()V
       7:   astore_1
       8:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
       11:  aload_1
       12:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       15:  aload_1
       16:  invokeinterface #6,  1; //InterfaceMethod InterfaceReference$IDance.dance:()V
       21:  new     #7; //class InterfaceReference$Snake
       24:  dup
       25:  invokespecial   #8; //Method InterfaceReference$Snake."<init>":()V
       28:  astore_2
       29:  getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
       32:  aload_2
       33:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       36:  aload_2
       37:  invokeinterface #6,  1; //InterfaceMethod InterfaceReference$IDance.dance:()V
       42:  return
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    从上面的字节码指令可以看到,dancer.dance();snake.dance(); 的字节码指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
    为什么invokeinterface指令会有两个参数呢?

    对象的内存模型如下所示:
    这里写图片描述
    从上图可以看到IDance接口中的方法dance()在Dancer类的方法表中的偏移量跟在Snake类的方法表中的偏移量是不一样的,因此无法仅根据偏移量来进行方法的调用。(这句话在理解时,要注意,只是为了强调invokeinterface在查找方法时不再是基于偏移量来实现的,而是基于搜索的方式。)应该这么说,dance方法在IDance方法表(如果有的话)中的偏移量与在Dancer方法表中的偏移量是不一样的。
    因此,要在Dancer的方法表中找到dance方法,必须搜索Dancer的整个方法表。

    下面写一个,如果Dancer中没有重写(override)toString方法,会发生什么?
    代码如下:

    package org.fan.learn.methodTable;
    
    /**
     * Created by fan on 2016/3/29.
     */
    public class InterfaceReference {
        interface IDance {
            void dance();
        }
    
        static class Person {
            @Override
            public String toString() {
                return "I'm a person";
            }
            public void speak() {
                System.out.println("Person speak");
            }
            public void eat() {
                System.out.println("Person eat");
            }
        }
    
        static class Dancer extends Person implements IDance {
    //        @Override
    //        public String toString() {
    //            return "I'm a Dancer";
    //        }
            @Override
            public void speak() {
                System.out.println("Dancer speak");
            }
            public void dance() {
                System.out.println("Dancer dance");
            }
        }
    
        static class Snake implements IDance {
            @Override
            public String toString() {
                return "I'm a Snake";
            }
            public void dance() {
                System.out.println("Snake dance");
            }
        }
    
        public static void main(String[] args) {
            IDance dancer = new Dancer();
            System.out.println(dancer);
            dancer.dance();
            //dancer.speak();
            //dancer.eat();
            IDance snake = new Snake();
            System.out.println(snake);
            snake.dance();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    执行结果如下:
    这里写图片描述
    可以看到System.out.println(dancer); 调用的是Person的toString方法。
    内存模型如下所示:
    这里写图片描述

    结束语

    这篇博文讨论了invokevirtual和invokeinterface的内部实现的区别,以及override的实现原理。下一步,打算讨论下invokevirtual的具体实现细节,如:如何实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。

  • 相关阅读:
    @Controller 与 @RestController 的区别
    Java泛型
    Java面试被经常问到的常用算法
    jdk和jre的区别
    Spring获取对象的方式
    xsi:schemaLocation的作用
    SpringBoot学习(一)
    docker-elk装IK自定义分词库
    MySQL存储引擎
    docker环境下elasticsearch安装ik和拼音分词
  • 原文地址:https://www.cnblogs.com/devilwind/p/7516381.html
Copyright © 2020-2023  润新知