序言
對於Java程序來說,最重要的特性就是他的靈活,和可拓展性,在Java剛剛面世的時候,一次編寫,處處運行無疑是最振奮和吸引程序員的,而在我們剛剛接觸Java的時候,Java的重載,繼承,多態性也讓人好奇,如此靈活的使用方式到底是從何而來,實現這一切的基石之一就是本篇所講的動態連接
我之前在虛擬機類加載機制(五)類解析的過程說過,其實對於Java程序來說,類的解析有時是不確定的,因爲重載,繼承等等動態語法只有在實際執行程序的時候才能確定具體調用的類,在此之前,Java無法得知具體的調用方法和類,自然也無法將符號轉換爲運行時的內存引用,所謂動態連接就是當確定具體的類或方法時,將符號動態解析爲對應的內存地址的過程
這就要涉及到jvm虛擬機中的分派問題了
所謂分派,就是jvm虛擬機確定具體類或方法的過程,分爲
- 靜態分派
- 動態分派
- 單分派
- 多分派
我們先聊一聊靜態分派
其實對於Java虛擬機來說,變量分兩種,靜態類型和實際類型,看下面這段代碼
public static void main(String[] args) {
Runnable runnable=new Thread();
}
對於Java虛擬機來說,Runnable就是靜態類型,無論裏面實際填充的是什麼,Runnable這個外殼是不會變的,對於Java虛擬機來說這是已經確定了的,但是實際類型是不一定的,可以是Thread,也可以是我們自己寫的類,只有運行時才能確定
而靜態分派,就是建立在靜態類型的基礎上的,靜態分派便是根據這一層殼來確定究竟要使用哪一個方法,由於在編譯之前,其實殼類型已經確定了,即使採用強轉等操作,對於虛擬機來說只不過是換了一層殼,殼本身依然是確定的
由此就解決了類似於下面這段代碼的問題
public static void main(String[] args) {
Runnable runnable=new Thread();
new Test().aa(runnable);
}
public void aa(Thread t){
System.out.println("Thread");
}
public void aa(Runnable r){
System.out.println("Runnable");
}
Java虛擬機會使用殼類型來確定最終使用的方法,最終打印Runnable
接下來是動態分派
上面的靜態分派確實可以確定不少方法的最終使用,但是有些方法最終的確定無法用靜態分派解決,比如下面的一段代碼
public static void main(String[] args) {
Runnable runnable=new Thread();
runnable.run();
}
如果用靜態分派解決,毫無疑問,這是一段錯誤的代碼,因爲接口並沒有實現run方法,但是稍有編程常識的人都知道,這是一段正常不過的代碼,讓我們分析一下字節碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new//創建Thread對象 #2 // class java/lang/Thread
3: dup//複製對象
4: invokespecial #3//調用對象的初始化方法,即,Thread對象的初始化方法 // Method java/lang/Thread."<init>":()V
7: astore_1//將棧頂元素彈出到本地變量表,即,賦值給runnable
8: aload_1//將runnable置於棧頂
9: invokeinterface #4, 1//調用接口方法,#4表示run方法,1表示該方法無參 // InterfaceMethod java/lang/Runnable.run:()V
14: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 args [Ljava/lang/String;
8 7 1 runnable Ljava/lang/Runnable;
我們可以很清楚的看出來,動態分派的本質就是找到對應類,並調用方法,即,使用實際類型來執行方法
單分派和多分派需要結合理解
對於Java來說,靜態分派決定分派的實際上是殼類型,但是如果我們使用如下的代碼
public static void main(String[] args) {
new Test().xx(1);
}
public void xx(int i){
}
public void xx(float i){
}
實際上兩個方法對於1來說都是可用方法,Java只是會選取一個最合適的方法來執行,但是不同的情況下最合適是不同的,根據多種情況分派就是多分派
相反,對於動態分派來說,只要確定了實際類型,就不會存在最合適這種說法,實際類型的方法是什麼,那麼最終執行的方法就是什麼,僅僅根據實際類型來分派就屬於單分派