前言
運行一個程序就是將PC寄存器的值設置爲程序入口地址,當有方法跳轉時就是將PC置爲方法的起始地址。在字節碼層面,一切方法調用在Class文件裏面存儲的都是符號引用,而不是方法在內存中的入口地址,這個特性給Java帶來了更強大的動態擴展能力。
這裏方法調用的意思是確定調用哪一個方法,不涉及方法內部的具體運行過程。爲什麼這是個問題呢?面向對象編程語言存在重載,重寫,虛方法的概念,有些情況下只有在運行時才能確定要執行的方法。
這裏稱之爲“劣質面試題”,是因爲正常情況下寫代碼不會出現這種寫法,並且無論對Java理解多深刻在工作中都應當極力避免出現這種寫法,這些題目是特意編出來考察對Java虛擬機方法調用的理解。
通用實現
過程即編程語言中的函數,方法,子例程,處理函數等,叫法不同但都是一個意思。假設過程P調用過程Q,Q執行完後返回到P,要實現這些操作需要:
- 傳遞控制:在進入Q時,要將PC設置爲Q代碼的起始地址,Q返回時要將PC設置爲P方法中調用Q後面的那條指令地址。
- 傳遞參數:P向Q提供參數,Q向P提供返回值。
- 分配和釋放內存:Q可能需要分配局部變量內存,執行完返回後要釋放這些內存。
實現過程調用機制的關鍵在於使用棧數據結構提供的後進先出的內存管理原則。
解析調用
如果調用一個方法沒有任何歧義,比如調用類的私有方法,就可以在編譯時確定要執行的方法。在類加載的解析階段,會將這部分符號引用轉化爲直接引用,這類方法的調用被稱爲解析。Java語言裏的靜態方法、私有方法、實例構造器、父類方法以及上被final 修飾的方法,這5種方法適用解析調用,這些方法統稱爲“非虛方法”,解析調用一定是個靜態的過程,在編譯期間就完全確定要執行的方法。其他方法被稱爲“虛方法”。
靜態分派
重載是方法名相同但參數類型或者個數不同,通常情況下參數數據類型有着很大的差異,比如:
public void a(String s) {}
public void a(int i) {}
public void a(String s,int i){}
//調用 a("ss")不會有任何歧義
但是,如果是這樣的呢:
//劣質面試題
class Human{}
class Man{}
class Woman{}
public void a(Human h) {}
public void a(Man m) {}
//調用
Human h=new Man();
a(h);
//實際上會執行a(Human h)
Human man = new Man();
對於這行代碼,靜態類型爲Human,實際類型爲Man,靜態類型是在編譯期可知的,實際類型變化的結果在運行期纔可確定。
// 實際類型變化的例子
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
虛擬機在重載時是通過參數的靜態類型而不是實際類型作爲判定依據的。所有依賴靜態類型來決定方法執行版本的分派動作,都稱爲靜態分派。
動態分派
class Human {
public void sayHello() {
System.out.println("human say hello");
}
}
class Man extends Human {
@Override
public void sayHello() {
System.out.println("man say hello");
}
}
public static void main(String[] args) {
Human human = new Man();
human.sayHello();//輸出man say hello
}
上述代碼的結果不會有歧義,但是根本原因是什麼?Java中實現重寫的本質是根據方法接收者的實際類型來選擇方法版本,具體實現是invokevirtual
指令。
invokevirtual
指令用於調用所有的虛方法,該指令的運行過程第一步是找到操作數棧頂的第一個元素所指向的對象的實際類型。根據上面虛方法/非虛方法的定義,能重寫的方法必然是個虛方法。
字段沒有多態性
invokevirtual
指令只針對方法,事實上Java裏面只有虛方法存在, 字段不可能是虛的,字段永遠不參與多態。當子類聲明瞭與父類同名的字段時,雖然在子類的內存中兩個字段都會存在,但是子類的字段會遮蔽父類的同名字段。
//劣質面試題
static class Father {
private int money;
public Father() {
money = 2;
//虛方法調用取決於實際類型
show();
}
public void show() {
System.out.println("Father:" + this.money);
}
}
static class Son extends Father {
private int money;
public Son() {
money = 4;
show();
}
@Override
public void show() {
//字段不參與多態,打印的永遠都是本類中的money
System.out.println("Son:" + this.money);
}
}
public static void main(String[] args) {
Father father = new Son();
System.out.println("father:" + father.money);
//輸出:
//Son:0
//Son:4
//father:2
}
Son類在創建的時候,首先隱式調用了Father的構造方法,而 Father構造方法中對show()
的調用是一次虛方法調用,這裏的實際類型是Son,所以實際執行的版本是 Son的show()
,又因爲此時Son還沒有執行構造方法,Son中money的值爲0,所以第一句輸出Son:0
。Son的構造方法執行完後,繼續執行Son的show(),輸出4,最後輸出Father類的中的money的值爲2。