虛擬機字節碼執行引擎-----方法調用
方法調用階段唯一的任務就是確定被調用方法的版本(調用的是哪一個方法),暫時還不涉及方法內部的具體運行過程。Class文件的編譯過程中 不包含傳統編譯過程中的“連接”,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存布局中的入口地址。這給java帶來更強的動態擴展功能的同時,也使用java方法的調用過程變得相對復雜起來,需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。
1.解析
在類加載的解析階段,會將一部分符號引用轉化為直接引用,這種解析能成立的前提是:方法在程序真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期是不可變得(調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來)這類方法的調用稱為解析。
在java語言中符合“編譯器可知,運行期不可變”的方法只有類方法和私有方法,因此他們都適用在類加載階段進行解析。
與之對應的是,java虛擬機裏面提供了五條方法調用字節碼指令。
invokestatic | 調用靜態方法 |
invokespecial | 調用實例構造器、私有方法和父類方法 |
invokevirtual | 調用所有的虛方法(可以被覆寫的方法都可以稱作虛方法,因此虛方法並不需要做特殊的聲明,也可以理解為除了用static、final、private修飾之外的所有方法都是虛方法。)註意:雖然final是用此指令調用,但並不是虛方法 |
invokeinterface | 調用接口方法,會在運行時再確定一個實現此接口的對象 |
invokedynamic | 先在運行時動態解析出調用點限定符所引用的方法,然後再執行該方法,在此之前的4條調用指令,分派邏輯是固化在java虛擬機內部的,而invokedynamic的分派邏輯是由用戶所設定的引導方法決定的 |
只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段中確定唯一的調用版本,靜態方法、實例構造器、私有方法和父類方法這四類在類加載的時候就會把符號引用解析為直接引用。這些方法可以被稱為非虛方法。
public class StaticResolution{public static void sayHello(){ System.out.println("hello world"); } public static void main(String[] args){ StaticResolution.sayHello(); } }
查看這段程序的字節碼
發現的確是通過invokestatic命令調用的sayhello();
解析調用是靜態的過程,在編譯期間就完全確定下來,在類加載的解析階段就會把涉及的符號引用全部轉變為可確定的直接引用,不會延遲到運行期再去完成。而分派調用則可能是靜態的也可能是動態的,根據分派的宗量數可分為單分派和多分派。
2.分派
可以通過分派調用過程揭示多態性特征的一些最基本的體現,如“重載”和“重寫”在java虛擬機中時如何實現的
①靜態分派
public class A{ static abstract class Human{} static class Man extends Human{} static class Woman extends Human{} public void sayHello(Human guy){ System.out.println("hello guy"); } public void sayHello(Human guy){ System.out.println("hello guy"); } public void sayHello(Human guy){ System.out.println("hello guy"); } public static void main(String[] args){ Human man=new Man(); Human women=new Woman(); A a=new A(); a.sayHello(man); a.sayHello(women); } }
輸出結果是
hello guy
hello guy
關於重載方法的使用,在方法接受者已經確定是對象“sr”的情況下,使用哪個重載版本,完全取決於傳入參數的數量和數據類型。代碼中可以地定義了兩個編譯期類型相同但運行期類型不同的變量,但編譯器在重載時是通過參數的編譯期類型而不是運行期類型作為判定依據的,並且編譯期數據在編譯期可知的,因此,在編譯階段,javac編譯期會根據傳入參數的編譯期類型決定使用哪個重載版本。
所有依賴編譯期類型來定位方法執行版本的分派動態稱為靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。另外,編譯期雖然能確定出方法的重載版本,但是在很多情況下這個重載版本並不是“唯一的”,往往是只能確定一個“更加合適的”版本,我們知道編寫代碼最重要的就是不能讓計算機糊塗,而這種模糊的概念很少見,這是因為字面量是不需要定義的,所以字面量沒有顯示的編譯期類型,它的編譯期類型只能通過語言上的規則去理解和推斷。
public class Overload{ public static void sayHello(Object arg){ System.out.println("hello Object"); } public static void sayHello(int arg){ System.out.println("hello int "); } public static void sayHello(long arg){ System.out.println("hello long"); } public static void sayHello(Character arg){ System.out.println("hello Character"); } public static void sayHello(char arg){ System.out.println("hello char"); } public static void sayHello(char... arg){ System.out.println("hello char..."); } public static void sayHello(Serializeable arg){ System.out.println("hello Serializeable "); } public static void main(String[] args){ sayHello(‘a‘); } }
輸出為
hello char
很好理解,因為‘a‘是char類型的數據,系統自然會選擇參數類型為char的重載方法,如果把char類型的重載方法去掉。
會輸出
hello int
這時發生了一次自動類型轉換,‘a’除了可以代表一個字符串,還可以代表數字97,因此參數類型為int的重載也是合適的,繼續去掉int類型的方法
輸出
hello long
發生了兩次自動類型轉換,char->int->long,實際上還可以繼續進行char->int->long->float->double,但是代碼中沒有float和double的重載方法,去掉long的方法
會輸出
hello Character
這是發生了一次自動裝箱。去掉Character類型。
輸出
hello Serializable
這是因為Character實現了Serializable,自動裝箱以後找不到裝箱類,只能向父類或接口轉型,這是如果Character還實現了一個接口,並且這個接口的重載類型也在上面代碼中,編譯器會糊塗,拒絕編譯。這時程序必須顯示地指定字面量的編譯器類型,再去掉這個方法
hello Object
這時是char裝箱後轉型為父類了,依次往上搜索。如果把這個也去掉
輸出就變成
hello char...
只剩下了這一個sayhello(char... org),說明變長參數的重載優先級是最低的。
這個例子演示了編譯期間選擇靜態分派目標的過程,這個過程也是java語言實現方法重載的本質。
註意:解析和分派之間並不是互斥的,而是是在不同層次上去篩選、確定目標方法的過程。例如:類方法在類加載的時候進行解析,但是類方法也會有重載版本,選擇重載版本的過程也是通過靜態分派完成的。
②動態分派
動態分派與重寫有著密切的關聯。
public class A{ static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ protected void sayHello(){ System.out.println("hello man"); } } static class Woman extends Human{ protected void sayHello(){ System.out.println("hello woman"); } } public static void main(String[] args){ Human man=new Man(); Human women=new Woman(); man.sayHello(); women.sayHello(); man=new Woman(); man.sayHello(); } }
虛擬機字節碼執行引擎-----方法調用