程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> java方法調用之動態調用多態(重寫override)的實現原理——方法表(三)

java方法調用之動態調用多態(重寫override)的實現原理——方法表(三)

編輯:JAVA綜合教程

java方法調用之動態調用多態(重寫override)的實現原理——方法表(三)


上兩篇篇博文討論了java的重載(overload)與重寫(override)、靜態分派與動態分派,這篇博文討論下動態分派的實現方法,即多態override的實現原理。
java方法調用之重載、重寫的調用原理(一)

本文大部分內容來自於IBM的博文多態在 Java 和 C++ 編程語言中的實現比較 。這裡寫一遍主要是加深自己的理解,方便以後查看,加入了一些自己的見解及行文組織,不是出於商業目的,如若需要下線,請告知。

結論

基於基類的調用和基於接口的調用,從性能上來講,基於基類的調用性能更高 。因為invokevirtual是基於偏移量的方式來查找方法的,而invokeinterface是基於搜索的。

概述

多態是面向對象程序設計的重要特性。多態允許基類的引用指向派生類的對象,而在具體訪問時實現方法的動態綁定。
java對方法動態綁定的實現方法主要基於方法表,但是這裡分兩種調用方式invokevirtual和invokeinterface,即類引用調用和接口引用調用。類引用調用只需要修改方法表的指針就可以實現動態綁定(具有相同簽名的方法,在父類、子類的方法表中具有相同的索引號),而接口引用調用需要掃描整個方法表才能實現動態綁定(因為,一個類可以實現多個接口,另外一個類可能只實現一個接口,無法具有相同的索引號。這句如果沒有看懂,繼續往下看,會有例子。寫到這裡,感覺自己看書時,有的時候也會不理解,看不懂,思考一段時間,還是不明白,做個標記,繼續閱讀吧,然後回頭再看,可能就豁然開朗。)。
類引用調用的大致過程為:java編譯器將java源代碼編譯成class文件,在編譯過程中,會根據靜態類型將調用的符號引用寫到class文件中。在執行時,JVM根據class文件找到調用方法的符號引用,然後在靜態類型的方法表中找到偏移量,然後根據this指針確定對象的實際類型,使用實際類型的方法表,偏移量跟靜態類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調用,否則,按照繼承關系從下往上搜索。
下面對上面的描述做具體的分析討論。

JVM的運行時結構

這裡寫圖片描述
從上圖可以看出,當程序運行時,需要某個類時,類載入子系統會將相應的class文件載入到JVM中,並在內部建立該類的類型信息,這個類型信息其實就是class文件在JVM中存儲的一種數據結構,他包含著java類定義的所有信息,包括方法代碼,類變量、成員變量、以及本博文要重點討論的方法表<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPqGj1eK49sDg0M3Qxc+ivs205rSi1Nq3vbeox/ihozxiciAvPg0K16LS4qOs1eK49re9t6jH+NbQtcTA4NDN0MXPorj61Nq20dbQtOa3xbXEY2xhc3O21M/zyseyu82stcSho9Tat723qMf41tCjrNXiuPZjbGFzc7XEwODQzdDFz6LWu9PQzqjSu7XEyrXA/aOoy/nS1MrHuPe49s/fs8y5ss/ttcTE2rTmx/jT8qOpo6y2+NTattHW0L/J0tTT0LbguPa4w2NsYXNzttTP86Gjv8nS1M2ouf220dbQtcRjbGFzc7bUz/O3w87Ktb23vbeox/jW0MDg0M3Qxc+ioaO+zc/x1NpqYXZht7TJ5Lv61sbEx9H5o6zNqLn9Y2xhc3O21M/zv8nS1LfDzsq1vbjDwOC1xMv509DQxc+i0rvR+aGjPGJyIC8+DQq3vbeose3Kx8q1z9a2r8ystffTw7XEusvQxKGjt723qLHttOa3xdTat723qMf41tC1xMDg0M3Qxc+i1tCho7e9t6ix7dbQtOa3xdPQuMPA4Lao0uW1xMv509C3vbeovLDWuM/yt723qLT6wuu1xNa41euho9Xi0Km3vbeo1tCw/MCotNO4uMDgvMyz0LXEy/nT0Le9t6jS1Lyw19TJ7dbY0LSjqG92ZXJyaWRlo6m1xLe9t6ihozwvcD4NCjxoMiBpZD0="類引用調用invokevirtual">類引用調用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();
    }
}

注意,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."":()V
   7:   astore_1
   8:   new     #4; //class ClassReference$Girl
   11:  dup
   12:  invokespecial   #5; //Method ClassReference$Girl."":()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

其中所有的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();
    }
}

上面的代碼中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."":()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."":()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

從上面的字節碼指令可以看到,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();
    }
}

執行結果如下:
這裡寫圖片描述
可以看到System.out.println(dancer); 調用的是Person的toString方法。
內存模型如下所示:
這裡寫圖片描述

結束語

這篇博文討論了invokevirtual和invokeinterface的內部實現的區別,以及override的實現原理。下一步,打算討論下invokevirtual的具體實現細節,如:如何實現符號引用到直接引用的轉換的?可能會看下OpenJDK底層的C++實現。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved