1. 程式人生 > >java橋接方法

java橋接方法

一個很典型的泛型(generic)程式碼。T是型別變數,可以是任何引用型別:

複製程式碼
public class Pair<T>{  
       private T first=null;  
       private T second=null;  
  
       public Pair(T fir,T sec){  
            this.first=fir;  
        this.second=sec;  
       }  
       public T getFirst(){  
             return this.first;  
       }  
       
public T getSecond(){ return this.second; } public void setFirst(T fir){ this.first=fir; } }
複製程式碼

1、Generic class 建立物件 

            Pair<String> pair1=new Pair("string",1);           ...①
            Pair<String> pair2=new Pair<String>("string",1)    ...②

有個很有趣的現象: ①程式碼在編譯期不會出錯,②程式碼在編譯期會檢查出錯誤。
這個問題其實很簡單
      (1) JVM本身並沒有泛型物件這樣的一個特殊概念。所有的泛型類物件在編譯器會全部變成普通類物件(這一點會在下面詳細闡述)。
      比如①,②兩個程式碼編譯器全部呼叫的是 Pair(Object fir, Object sec)這樣的構造器。

      因此程式碼①中的new Pair("string",1)在編譯器是沒有問題的,畢竟編譯器並不知道你建立的Pair型別中具體是哪一個型別變數T,而且編譯器肯定了String物件和Integer物件都屬於Object型別的。

       但是一段執行pair1.getSecond()就會丟擲ClassCastException異常。這是因為JVM會根據第一個引數"string"推算出T型別變數是String型別,這樣getSecond也應該是返回String型別,然後編譯器已經默認了second的運算元是一個值為1的Integer型別。當然就不符合JVM的執行要求了,不終止程式才怪。


       (2) 但程式碼②會在編譯器報錯,是因為new Pair<String>("string",1)已經指明瞭建立物件pair2的型別變數T應該是String的。所以在編譯期編譯器就知道錯誤出在第二個引數Integer了。


小結一下:
建立泛型物件的時候,一定要指出型別變數T的具體型別。爭取讓編譯器檢查出錯誤,而不是留給JVM執行的時候丟擲異常。

2、JVM如何理解泛型概念 —— 型別擦除 
    事實上,JVM並不知道泛型,所有的泛型在編譯階段就已經被處理成了普通類和方法。
    處理方法很簡單,我們叫做型別變數T的擦除(erased) 
    無論我們如何定義一個泛型型別,相應的都會有一個原始型別被自動提供。原始型別的名字就是擦除型別引數的泛型型別的名字。
         如果泛型型別的型別變數沒有限定(<T>) ,那麼我們就用Object作為原始型別;
         如果有限定(<T extends XClass>),我們就XClass作為原始型別;
         如果有多個限定(<T extends XClass1&XClass2>),我們就用第一個邊界的型別變數XClass1類作為原始型別;

    比如上面的Pair<T>例子,編譯器會把它當成被Object原始型別替代的普通類來替代。

複製程式碼
//編譯階段:型別變數的擦除  
public class Pair{  
       private Object first=null;  
       private Object second=null;  
  
       public Pair(Object fir,Object sec){  
           this.first=fir;  
           this.second=sec;  
       }  
      public Object getFirst(){  
           return this.first;  
      }  
      public void setFirst(Object fir){  
           this.first=fir;  
      }  
   }  
複製程式碼

3、泛型約束和侷限性—— 型別擦除所帶來的麻煩

(1)  繼承泛型型別的多型麻煩。(—— 子類沒有覆蓋住父類的方法 )

     看看下面這個類SonPair

class SonPair extends Pair<String>{  
          public void setFirst(String fir){....}  
}  

 很明顯,程式設計師的本意是想在SonPair類中覆蓋父類Pair<String>的setFirst(T fir)這個方法。但事實上,SonPair中的setFirst(String fir)方法根本沒有覆蓋住Pair<String>中的這個方法。
     原因很簡單,Pair<String>在編譯階段已經被型別擦除為Pair了,它的setFirst方法變成了setFirst(Object fir)。 那麼SonPair中setFirst(String)當然無法覆蓋住父類的setFirst(Object)了。

這對於多型來說確實是個不小的麻煩,我們看看編譯器是如何解決這個問題的。

編譯器 會自動在 SonPair中生成一個橋方法(bridge method ) : 
           public void setFirst(Object fir){
                   setFirst((String) fir)
            } 

這樣,SonPair的橋方法確實能夠覆蓋泛型父類的setFirst(Object) 了。而且橋方法內部其實呼叫的是子類位元組setFirst(String)方法。對於多型來說就沒問題了。

 問題還沒有完,多型中的方法覆蓋是可以了,但是橋方法卻帶來了一個疑問:

現在,假設 我們還想在 SonPair 中覆蓋getFirst()方法呢?

class SonPair extends Pair<String>{  
      public String getFirst(){....}  
}  

 由於需要橋方法來覆蓋父類中的getFirst,編譯器會自動在SonPair中生成一個 public Object getFirst()橋方法。 
 但是,疑問來了,SonPair中出現了兩個方法簽名一樣的方法(只是返回型別不同):

            ①String getFirst()   // 自己定義的方法
            ②Object getFirst()  //  編譯器生成的橋方法 

      難道,編譯器允許出現方法簽名相同的多個方法存在於一個類中嗎?

      事實上有一個知識點可能大家都不知道:
      ① 方法簽名 確實只有方法名+引數列表 。這毫無疑問!
      ② 我們絕對不能編寫出方法簽名一樣的多個方法 。如果這樣寫程式,編譯器是不會放過的。這也毫無疑問!
      ③ 最重要的一點是:JVM會用引數型別和返回型別來確定一個方法。 一旦編譯器通過某種方式自己編譯出方法簽名一樣的兩個方法(只能編譯器自己來創造這種奇蹟,我們程式設計師卻不能人為的編寫這種程式碼)。JVM還是能夠分清楚這些方法的,前提是需要返回型別不一樣。

(2) 泛型型別中的方法衝突

//在上面程式碼中加入equals方法  
public class Pair<T>{  
      public boolean equals(T value){  
            return (first.equals(value));  
      }  
}  

這樣看似乎沒有問題的程式碼連編譯器都通過不了:

       【Error】    Name clash: The method equals(T) of type Pair<T> has the same erasure as equals(Object) of type Object but does not override it。

        編譯器說你的方法與Object中的方法衝突了。這是為什麼?

        開始我也不太明白這個問題,覺得好像編譯器幫助我們使得equals(T)這樣的方法覆蓋上了Object中的equals(Object)。經過大家的討論,我覺得應該這麼解釋這個問題?

        首先、我們都知道子類方法要覆蓋,必須與父類方法具有相同的方法簽名(方法名+引數列表)。而且必須保證子類的訪問許可權>=父類的訪問許可權。這是大家都知道的事實。

        然後、在上面的程式碼中,當編譯器看到Pair<T>中的equals(T)方法時,第一反應當然是equals(T)沒有覆蓋住父類Object中的equals(Object)了。

        接著、編譯器將泛型程式碼中的T用Object替代(擦除)。突然發現擦除以後equals(T)變成了equals(Object),糟糕了,這個方法與Object類中的equals一樣了。基於開始確定沒有覆蓋這樣一個想法,編譯器徹底的瘋了(精神分裂)。然後得出兩個結論:①堅持原來的思想:沒有覆蓋。但現在一樣造成了方法衝突了。   ②寫這程式的程式設計師瘋了(哈哈)。

        再說了,拿Pair<T>物件和T物件比較equals,就像牛頭對比馬嘴,哈哈,邏輯上也不通呀。

(3) 沒有泛型陣列一說

      Pair<String>[] stringPairs=new Pair<String>[10];
      Pair<Integer>[] intPairs=new Pair<Integer>[10];

      這種寫法編譯器會指定一個Cannot create a generic array of Pair<String>的錯誤

      我們說過泛型擦除之後,Pair<String>[]會變成Pair[],進而又可以轉換為Object[];

      假設泛型陣列存在,那麼

            Object[0]=stringPairs[0]; Ok
            Object[1]=intPairs[0]; Ok

       這就麻煩了,理論上將Object[]可以儲存所有Pair物件,但這些Pair物件是泛型物件,他們的型別變數都不一樣,那麼呼叫每一個Object[]陣列元素的物件方法可能都會得到不同的記過,也許是個字串,也許是整形,這對於JVM可是無法預料的。

      記住: 陣列必須牢記它的元素型別,也就是所有的元素物件都必須一個樣,泛型型別恰恰做不到這一點。即使Pair<String>,Pair<Integer>... 都是Pair型別的,但他們還是不一樣。


總結:泛型程式碼與JVM 
    ① 虛擬機器中沒有泛型,只有普通類和方法。
    ② 在編譯階段,所有泛型類的型別引數都會被Object或者它們的限定邊界來替換。(型別擦除)
    ③ 在繼承泛型型別的時候,橋方法的合成是為了避免型別變數擦除所帶來的多型災難。