JVM學習之:淺談方法呼叫以及Override/Overload的原理
提到方法呼叫,我想大多數人的第一反應就是執行一個方法唄,其實在虛擬機器的眼裡方法呼叫只是確定他要呼叫哪個方法而已,和方法的執行還是有比較大的區別的.任何一個層序的執行都離不開方法的呼叫以及方法的執行,但是在JVM學習之:虛擬機器中的執行時棧幀總結(二)提到過,在Class檔案的編譯過程中不包括傳統的連線步驟(連線:把符號引用轉化為可以直接找到方法體的直接引用),但是正是因為這點也給java帶來了更大的靈活性,因為不同的實現可能會在不同的階段對符號引用進行轉化,下面是對幾種常見的方法呼叫型別進行描述
解析:
在JVM學習之:虛擬機器中的執行時棧幀總結中提到了,如果符號引用是在類載入階段或者第一次使用的時候轉化為直接應用,那麼這種轉換成為靜態解析
前面提到了幾種方法的呼叫,虛擬機器也提供了對應的位元組碼指令,分別是:
invokestatic:呼叫靜態方法
invokespecial:呼叫構造器方法,私有方法以及父類方法
invokevirtual:呼叫虛方法以及final方法(雖然用invokevirtual呼叫,可是因為final方法的不可覆蓋性,因此也是非虛方法)
invokeinterface:呼叫介面方法,會在執行時再確定一個具體的實現方法下面通過一個演示類的反編譯結果來對上面的位元組碼指令進行驗證:
package com.eric.jvm.executor;
/**
* 通過反編譯位元組碼來驗證
* invokestatic:呼叫靜態方法
* invokespecial:呼叫構造器方法,私有方法以及父類方法
* invokevirtual:呼叫虛方法以及final方法(雖然用invokevirtual呼叫,可是因為final方法的不可覆蓋性,因此也是非虛方法)
* invokeinterface:呼叫介面方法,會在執行時再確定一個具體的實現方法
*
*
* javap -verbose com.eric.jvm.executor.InvokeCommandExecutor
*
* @author Eric
*
*/
public class InvokeCommandExecutor {
public static void main(String[] args) {
//invokestatic
SubInvoker.invokeStatic();
//invokespecial,
SubInvoker si=new SubInvoker();
//invokevirtual
si.invokeVirtual();
//invokeinterface
si.invokeInterface();
}
}
class SubInvoker implements IExecutor{
public static void invokeStatic(){
System.out.println("invokestatic was execute");
}
public SubInvoker(){
System.out.println("invokespecial was execute in construct");
};
public void invokeVirtual(){
System.out.println("invokevirtual was execute");
}
@Override
public void invokeInterface() {
System.out.println("invokeinterface was execute");
}
}
interface IExecutor{
public void invokeInterface();
}
反編譯後的相關的指令片段:對應main中的方法呼叫順序
分派:
眾所周知,面向物件的三個特點是:"繼承,封裝,多型",其中多型又包括覆蓋和過載,本節提到的分派就是覆蓋和過載的底層實現基礎.那麼讓我們來看看什麼是分派?分派和解析屬於同一個範疇的概念,都是方法呼叫的型別而已,只是分派比解析要稍微的複雜一點,分派的符號應用可以再類載入階段進行轉換,也可以再執行時進行轉換,而且根據宗量也可能存在單個宗量以及多個宗量,下面將分別對其種類進行說明:
靜態分派(Overload):
靜態分派可以認為是java可是實現overload的最根本的原因,首先讓我們來看一個關於overload的例子(在寫這篇文章之間我還是沒能猜出正確的輸出,正是可悲啊...)
package com.eric.jvm.executor;
public class MethodOverloadResolution {
public static void main(String[] args) {
Human human = new Human();
Human man = new Man();
Human women = new Women();
MethodOverloadResolution mor = new MethodOverloadResolution();
mor.hello(human);
mor.hello(man);
mor.hello(women);
//靜態型別發生變化
mor.hello((Man)man);
mor.hello((Women)women);
}
public void hello(Human human) {
System.out.println("Human say hello");
}
public void hello(Man human) {
System.out.println("Man say hello");
}
public static void hello(Women human) {
System.out.println("Women say hello");
}
}
class Human {}
class Man extends Human {}
class Women extends Human {}
其輸出的結果是
Human say hello
Human say hello
Human say hello
Man say hello
Women say hello
我想經常面試的人可以很順利的猜到正確的輸出結果,可是如果要是問一下到底為什麼會這樣可能很多人就答不出來了,下面我來對為什麼會輸出上面這樣的結果來解釋一下,
Human man = new Man();
首先看看兩個比較重要的概念,Human是靜態型別,Man是動態型別,靜態型別是在編譯期間確定的,而且虛擬機器的過載依據是根據靜態型別類確定的,所以在編譯階段,虛擬機器已經決定好了要是用哪個版本的方法這也是輸出3個Human say hello的原因.
依賴靜態型別類確定方法版本的分派動作方式稱為靜態分派
關於靜態分派還有一點比較重要的就是,在選擇方法版本的時候是選擇一個"最適合的",下面通過一個例子來看看到底什麼是"最適合的":
import java.io.Serializable;
public class MethodOverloadDemonstrate {
/**
* @param args
*/
public static void main(String[] args) {
MethodOverloadDemonstrate demonstrate=new MethodOverloadDemonstrate();
demonstrate.overLoad('c');
}
public void overLoad(char c){System.out.println("char");}
public void overLoad(int c){System.out.println("int");}
public void overLoad(long c){System.out.println("long");}
public void overLoad(float c){System.out.println("float");}
public void overLoad(double c){System.out.println("double");}
public void overLoad(Character c){System.out.println("Character");}
public void overLoad(Serializable c){System.out.println("Serializable");}
public void overLoad(Object... c){System.out.println("Object...");}
public void overLoad(Object c){System.out.println("Object");}
public void overLoad(Integer c){System.out.println("Integer");}
public void overLoad(Comparable<Character> c){System.out.println("Comparable<Character>");}
}
當註釋掉public void overLoad(char c){System.out.println("char");}方法時,編譯器並沒有報錯,而是選擇了public void overLoad(int c){System.out.println("int");},同樣當註釋掉public void overLoad(int c){System.out.println("int");}時,會繼續將型別進行擴大,以此類推,呼叫方法的優先順序順序為
char->int->long->float->double->Character->介面(Serilizable)->父類(Object)->Object....objs
有一點需要注意的是,因為Character extends Object implements java.io.Serializable, Comparable<Character>,如果註釋掉Serilizable型別引數前面的方法,則因為Serializable 和Compareable<Character>都是介面,所以有同樣的優先順序,這回讓編譯器不能確認具體的方法版本,會拒絕編譯
動態分派(Override):
動態分派與java的另外一個重要特性,重寫(Override)有這非常緊密的關係,下面通過一個簡單的例子來說明虛擬機器到底是如何實現Override的
package com.eric.jvm.executor;
/**
* 關於Override的例子
*
* @author Eric
*
*/
public class MethodOverrideDemonstrate {
public static void main(String[] args) {
MethodOverrideDemonstrate demonstrate = new MethodOverrideDemonstrate();
Parent parent = demonstrate.new Parent();
Parent father = demonstrate.new Father();
Parent mother = demonstrate.new Mother();
parent.printInfo();
father.printInfo();
mother.printInfo();
}
class Parent {
public void printInfo() {
System.out.println("Parent output....");
}
}
class Father extends Parent {
public void printInfo() {
System.out.println("Father output....");
}
}
class Mother extends Parent {
public void printInfo() {
System.out.println("Mother output....");
}
}
}
//output
Parent output....
Father output....
Mother output....
因為靜態分派是通過靜態型別類確定方法版本的,所以可以推斷出動態分派是根據動態型別類確定方法版本的,更具體的說是因為在例子中printInfo()方法為虛方法,執行虛方法的虛擬機器指令為invokevirtual.而該指令的執行過程如下,由此可見,動態型別對於Override的重要性
單分派,多分派
方法的接受者以及方法的方法的引數統稱為宗量.單分派即用一個宗量來確定執行的方法,多分派就是用多個方法來確定被執行的方法
package com.eric.jvm.executor;
public class SingleMultiDispatch {
/**
* 演示單分派,多分派
*
* @param args
*/
public static void main(String[] args) {
Parent parent=new Parent();
Parent son=new Son();
parent.buy(new IPhone());
son.buy(new Sumsung());
}
static class Parent{
public void buy(IPhone ipPhone){System.out.println("parent buy iphone");};
public void buy(Sumsung sumsung){System.out.println("parent buy sumsung");};
}
static class Son extends Parent{
public void buy(IPhone ipPhone){System.out.println("son buy iphone");};
public void buy(Sumsung sumsung){System.out.println("son buy sumsung");};
}
static class Sumsung {}
static class IPhone {}
}
編譯階段的方法選擇過程(靜態分派)為,首先根據靜態型別判斷是Parent還是Son類確定靜態型別,然後還要根據引數來決定動態型別,所以靜態分派為多分派
執行階段虛擬機器選擇的過程(動態分派)為,首先在編譯階段確定了方法簽名位buy(Sumsung),所以這個時候引數已經確定了,唯一不能確定的就是方法的實際接受者,因此只有一個宗量,即動態分派為單分派