zoukankan      html  css  js  c++  java
  • 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++实现。

  • 相关阅读:
    zoj 3627#模拟#枚举
    Codeforces 432D Prefixes and Suffixes kmp
    hdu 4778 Gems Fight! 状压dp
    CodeForces 379D 暴力 枚举
    HDU 4022 stl multiset
    手动转一下田神的2048
    【ZOJ】3785 What day is that day? ——KMP 暴力打表找规律
    poj 3254 状压dp
    C++中运算符的优先级
    内存中的数据对齐
  • 原文地址:https://www.cnblogs.com/devilwind/p/7516381.html
Copyright © 2011-2022 走看看