今天看到《Java核心技術I》書上的動態綁定,意思就是當子類和父類存在同一個方法,子類重寫了父類的方法,程序在運行時調用方法是調用父類的方法還是子類的重寫方法呢?程序會在運行的時候自動選擇調用某個方法(根據方法表)。
看完這裏不由自主的想到,有動態肯定也就有靜態吧,於是去求助了下google,首先看了下什麼是綁定:
綁定指的是一個方法的調用與方法所在的類(方法主體)關聯起來。對java來說,綁定分爲靜態綁定和動態綁定;或者叫做前期綁定和後期綁定。
然後我們分別看看兩者之間含義以及差別
動態綁定:在運行時根據具體對象的類型進行綁定。若一種語言實現了後期綁定,同時必須提供一些機制,可在運行期間判斷對象的類型,並分別調用適當的方法。也就是說,編譯器此時依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。不同的語言對後期綁定的實現方法是有所區別的。但我們至少可以這樣認爲:它們都要在對象中安插某些特殊類型的信息。
動態綁定的過程:
虛擬機提取對象的實際類型的方法表;
虛擬機搜索方法簽名;
調用方法。
靜態綁定:在程序執行前方法已經被綁定(也就是說在編譯過程中就已經知道這個方法到底是哪個類中的方法),此時由編譯器或其它連接程序實現。針對java,可以簡單的理解爲程序編譯期的綁定;這裏要特別說明一點,java當中的方法只有final,static,private和構造方法是前期綁定。
差別:其實上述解釋可以看出很多東西了。
(1)靜態綁定發生在編譯時期,動態綁定發生在運行時
(2)使用private或static或final修飾的變量或者方法,使用靜態綁定。而虛方法(可以被子類重寫的方法)則會根據運行時的對象進行動態綁定。
(3)靜態綁定使用類信息來完成,而動態綁定則需要使用對象信息來完成。
(4)重載(Overload)的方法使用靜態綁定完成,而重寫(Override)的方法則使用動態綁定完成。
下面開始代碼測試:
public class Test {
public static void main(String[] args) {
String str = new String();
Lee lee = new Lee();
lee.say(str);
}
static class Lee {
public void say(Object obj) {
System.out.println("這是個Object");
}
public void say(String str) {
System.out.println("這是個String");
}
}
}
執行結果:
$ java Test
這是個String
在上面的代碼中,lee方法存在兩個重載的實現,一個是接收Object類型的對象作爲參數,另一個則是接收String類型的對象作爲參數。而str是一個String對象,所有接收String類型參數的call方法會被調用。而這裏的綁定就是在編譯時期根據參數類型進行的靜態綁定。
接着我們反編譯驗證一下:
javap -c Test
Compiled from "Test.java"
public class CoreJava.day_2.Test {
public CoreJava.day_2.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
8: new #4 // class CoreJava/day_2/Test$Lee
11: dup
12: invokespecial #5 // Method CoreJava/day_2/Test$Lee."<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: invokevirtual #6 // Method CoreJava/day_2/Test$Lee.call:(Ljava/lang/String;)V
21: return
}
看到了這一行18: invokevirtual #6 // Method CoreJava/day_2/Test$Lee.call:(Ljava/lang/String;)V確實是發生了靜態綁定,確定了調用了接收String對象作爲參數的say方法。
現在可以改寫一下:
public class Test{
public static void main(String[] args) {
String str = new String();
Lee lee = new SecLee();
lee.say(str);
}
static class Lee {
public void say(String str) {
System.out.println("這是個String");
}
}
static class SecLee extends Lee {
@Override
public void say(String str) {
System.out.println("這是第二李的String");
}
}
}
結果爲:
$ java Test
這是第二李的String
上面,用SecLee繼承了Lee,並且重寫了say方法。我們聲明瞭一個Lee類型的變量lee,但是這個變量指向的是他的子類SecLee。根據結果可以看出,其調用了SecLee的say方法實現,而不是Lee的say方法。這一結果的產生的原因是因爲在運行時發生了動態綁定,在綁定過程中需要確定調用哪個版本的say方法實現。
再看看反編譯的結果:
javap -c Test
警告: 二進制文件Test包含CoreJava.day_2.Test
Compiled from "Test.java"
public class CoreJava.day_2.Test {
public CoreJava.day_2.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
8: new #4 // class CoreJava/day_2/Test$SecLee
11: dup
12: invokespecial #5 // Method CoreJava/day_2/Test$SecLee."<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: invokevirtual #6 // Method CoreJava/day_2/Test$Lee.say:(Ljava/lang/String;)V
21: return
}
正如上面的結果,18: invokevirtual #6 // Method CoreJava/day_2/Test Lee.say:(Ljava/lang/String;)V這裏是TestLee.say而非Test$SecLee.say,因爲編譯期無法確定調用子類還是父類的實現,所以只能丟給運行時的動態綁定來處理。
既然重寫測試了,那我們再試試重載:
下面的例子更復雜!Lee類中存在say方法的兩種重載,更復雜的是SecLee集成Lee並且重寫了這兩個方法。其實這種情況是上面兩種情況的複合情況。
下面的代碼首先會發生靜態綁定,確定調用參數爲String對象的say方法,然後在運行時進行動態綁定確定執行子類還是父類的say實現。
public class Test {
public static void main(String[] args) {
String str = new String();
Lee lee = new SecLee();
lee.say(str);
}
static class Lee {
public void say(Object obj) {
System.out.println("這是Object");
}
public void say(String str) {
System.out.println("這是String");
}
}
static class SecLee extends Lee {
@Override
public void say(Object obj) {
System.out.println("這是第二李的Object");
}
@Override
public void say(String str) {
System.out.println("這是第二李的String");
}
}
}
結果:
$ java Test
這是第二李的String
結果在意料之中,就不多說了。
那麼問題來了,非動態綁定不可麼?
其實某些方法的綁定也可以由靜態綁定實現,比如說:
public static void main(String[] args) {
String str = new String();
final Lee lee = new SecLee();
lee.say(str);
}
可以看出,這裏lee持有SecLee的對象並且lee變量爲final,立即執行了say方法,編譯器理論上通過足夠的分析代碼,是可以知道應該調用SecLee的say方法。
結論:
由於動態綁定需要在運行時確定執行哪個版本的方法實現或者變量,比起靜態綁定起來要耗時,所以正如書上所說的,有些程序員認爲,除非有足夠的理由使用多態性,應該把所有的方法都聲明爲final,private或者static進行修飾。我覺得這個有點偏激了,具體使用仁者見仁,智者見智吧。