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++實現。