Java的靜態/動態綁定

  今天看到《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進行修飾。我覺得這個有點偏激了,具體使用仁者見仁,智者見智吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章