上兩篇篇博文討論了java的重載(overload)與重寫(override)、靜態分派與動態分派,這篇博文討論下動態分派的實現方法,即多態override的實現原理。
java方法調用之重載、重寫的調用原理(一)
本文大部分內容來自於IBM的博文多態在 Java 和 C++ 編程語言中的實現比較 。這裡寫一遍主要是加深自己的理解,方便以後查看,加入了一些自己的見解及行文組織,不是出於商業目的,如若需要下線,請告知。
基於基類的調用和基於接口的調用,從性能上來講,基於基類的調用性能更高 。因為invokevirtual是基於偏移量的方式來查找方法的,而invokeinterface是基於搜索的。
多態是面向對象程序設計的重要特性。多態允許基類的引用指向派生類的對象,而在具體訪問時實現方法的動態綁定。
java對方法動態綁定的實現方法主要基於方法表,但是這裡分兩種調用方式invokevirtual和invokeinterface,即類引用調用和接口引用調用。類引用調用只需要修改方法表的指針就可以實現動態綁定(具有相同簽名的方法,在父類、子類的方法表中具有相同的索引號),而接口引用調用需要掃描整個方法表才能實現動態綁定(因為,一個類可以實現多個接口,另外一個類可能只實現一個接口,無法具有相同的索引號。這句如果沒有看懂,繼續往下看,會有例子。寫到這裡,感覺自己看書時,有的時候也會不理解,看不懂,思考一段時間,還是不明白,做個標記,繼續閱讀吧,然後回頭再看,可能就豁然開朗。)。
類引用調用的大致過程為:java編譯器將java源代碼編譯成class文件,在編譯過程中,會根據靜態類型將調用的符號引用寫到class文件中。在執行時,JVM根據class文件找到調用方法的符號引用,然後在靜態類型的方法表中找到偏移量,然後根據this指針確定對象的實際類型,使用實際類型的方法表,偏移量跟靜態類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調用,否則,按照繼承關系從下往上搜索。
下面對上面的描述做具體的分析討論。
代碼如下: 注意, 其中所有的invokevirtual調用的都是Person類中的方法。 下面看看java對象的內存模型: 下面再看看調用過程,以 代碼如下: 上面的代碼中 從上面的字節碼指令可以看到, 對象的內存模型如下所示: 下面寫一個,如果Dancer中沒有重寫(override)toString方法,會發生什麼? 執行結果如下: 這篇博文討論了invokevirtual和invokeinterface的內部實現的區別,以及override的實現原理。下一步,打算討論下invokevirtual的具體實現細節,如:如何實現符號引用到直接引用的轉換的?可能會看下OpenJDK底層的C++實現。
從上圖可以看出,當程序運行時,需要某個類時,類載入子系統會將相應的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."
從上圖可以清楚地看到調用方法的指針指向。而且可以看出相同簽名的方法在方法表中的偏移量是一樣的。這個偏移量只是說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."
dancer.dance();
和snake.dance();
的字節碼指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
。
為什麼invokeinterface指令會有兩個參數呢?
從上圖可以看到IDance接口中的方法dance()在Dancer類的方法表中的偏移量跟在Snake類的方法表中的偏移量是不一樣的,因此無法僅根據偏移量來進行方法的調用。(這句話在理解時,要注意,只是為了強調invokeinterface在查找方法時不再是基於偏移量來實現的,而是基於搜索的方式。)應該這麼說,dance方法在IDance方法表(如果有的話)中的偏移量與在Dancer方法表中的偏移量是不一樣的。
因此,要在Dancer的方法表中找到dance方法,必須搜索Dancer的整個方法表。
代碼如下:
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方法。
內存模型如下所示:
結束語