1. 程式人生 > >Retrofit完全解析(二):泛型(Generic)

Retrofit完全解析(二):泛型(Generic)

Retrofit原始碼閱中,發現自帶暈眩光環的Type(簡直無CD),而談到Type繞不過泛型,借這個機會好好捋捋。

1.泛型的由來

引入版本: Java平臺在JDK 5中引入了一個重要的特性 —— 泛型(generics)(又被稱作引數化型別 parameterized type),。

泛型引入的目:為了解決容器(資料結構)的型別安全性,使得編譯器在編譯時就能發現明顯的型別錯誤,從而避免執行時的轉型錯誤(不細說了)。(泛型的優點不僅如此,見下文)

2.泛型實現原理

通常情況下,一個編譯器處理泛型有兩種方式:

  • Code specialization。在例項化一個泛型類或泛型方法時都產生一份新的目的碼(位元組碼or二進位制程式碼)。例如,針對一個泛型list,可能需要 針對string,integer,float產生三份目的碼。
  • Code sharing。對每個泛型類只生成唯一的一份目的碼;該泛型類的所有例項都對映到這份目的碼上,在需要的時候執行型別檢查和型別轉換。

C++中的模板(template)是典型的Code specialization實現。C++編譯器會為每一個泛型類例項生成一份執行程式碼。執行程式碼中integer list和string list是兩種不同的型別。
Code specialization會導致:

  • 程式碼膨脹(code bloat),不過有經驗的C++程式設計師可以有技巧的避免程式碼膨脹。
  • 在引用型別系統中,浪費空間,因為引用型別集合中元素本質上都是一個指標。沒必要為每個型別都產生一份執行程式碼。而這也是Java編譯器中採用Code sharing方式處理泛型的主要原因。

Java編譯器通過Code sharing方式為每個泛型型別建立唯一的位元組碼錶示,並且將該泛型型別的例項都對映到這個唯一的位元組碼錶示上。將多種泛型類形例項對映到唯一的位元組碼錶示是通過型別擦除(type erasue)實現的。(省程式碼,省記憶體)

3.泛型的優點

  • 型別安全
    泛型的主要目標是提高 Java 程式的型別安全。通過知道使用泛型定義的變數的型別限制,編譯器可以在一個高得多的程度上驗證型別假設。沒有泛型,這些假設就只存在於程式設計師的頭腦中(或者如果幸運的話,還存在於程式碼註釋中)。

  • 消除強制型別轉換(型別推斷)
    泛型的一個附帶好處是,消除原始碼中的許多強制型別轉換。這使得程式碼更加可讀,並且減少了出錯機會。

         // 強制型別轉換
         List dataList = new ArrayList();
         String dataS = (String) dataList.get(0);
         // 泛型
         List<String> strList = new ArrayList();
         String strS = strList.get(0);
  • 避免程式碼膨脹 (相對於C++的泛型實現而言)(Java泛型的型別擦除機制)

  • 潛在的效能收益(有點暈,不太明白)
    泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制型別轉換(沒有泛型的話,程式設計師會指定這些強制型別轉換)插入生成的位元組碼中。但是更多型別資訊可用於編譯器這一事實,為未來版本的 JVM 的優化帶來可能。由於泛型的實現方式,支援泛型(幾乎)不需要 JVM 或類檔案更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型(和強制型別轉換)時所寫的程式碼,只是更能確保型別安全而已。

4.泛型的兩個重要概念

  • 型別推斷(Type Inference)

          static <T> T pick(T a1, T a2) {  
                 return a2;  
          }  

    靜態方法pick()在三個地方使用了泛型,分別限定了兩個輸入引數的型別與返回型別。呼叫該方法的程式碼如下:

        // 通過輸入值,推斷返回值
        Integer ret = pick(new Integer(1), new Integer(2));  

    兩個輸入引數為不同的型別,應該返回什麼:java編譯器就會根據這兩個引數型別來推斷,儘量使返回型別為最明確的一種。
    示例:”d”, new ArrayList() 兩個引數, String與ArrayList都實現了同樣的介面——Serializable

  • 型別擦除(Type Erasure)

    先看一個編譯錯誤

    Cannot perform instanceof check against parameterized type Set<Integer>. Use the form Set<?> instead since further generic type information will be erased at runtime
    
    erased at runtime 這個是啥意思呢? 就是JDK在引入泛型時候,為了相容1.5以前的版本,Set<Integer>其實是跟Set一個型別的,也就是說Set<Integer>Set<String>在執行時是一種型別,會被消除型別,只是在編譯時候的強型別檢查.

    也就是說在編譯之後泛型會被擦除,變為1.5之前沒有泛型的狀態,T會被Object替代(所以泛型不能使用基本資料型別),Map集合的型別限制泛型會被擦除。

    那麼泛型僅僅在編譯器起資料檢查作用麼?答案是:No,Java 1.5引入泛型的同時還引入了介面Type,甚至可以在執行期通過反射獲取泛型的資訊,Type系列的介面用法會在下篇進行總結歸納。

5.泛型的注意小知識

  • 泛型類的繼承
    Integer和Double都是Number的子類,但是Box與Box並不是Box的子類,不存在繼承關係。Box與Box的共同父類是Object。
  • 泛型不能用基本型別表示
    上面提過,必須是Object及其子類
  • 不可例項化型別引數
    程式碼中還未清楚某次執行期的具體物件,故無法直接建立。
    但是執行期可以通過某些反射,先拿型別,再進行物件建立。
  • 不能在靜態欄位上使用泛型
    靜態變數是所有例項共享的,泛型是用來限制某個物件的型別,限制某方法的型別呼叫
  • 不能對帶有引數化型別的類使用cast或instanceof方法

        public static<E> void rtti(List<E> list) {  
            if (list instanceof ArrayList<Integer>){  // 編譯錯誤  
                // ...  
            }  
        }  

    因為執行期list型別必然僅僅是List,而List instanceof ArrayList無意義,且邏輯不對

  • 不能建立帶有引數化型別的陣列(陣列是不支援泛型的)

  • 不能建立、捕獲泛型異常
    泛型執行期是被擦除的,資料是儲存在父類的各種Type中的,此時與泛型無關了
  • 不能過載經過型別擦除後形參轉化為相同原始型別的方法

        public class Example {  
            public void print(Set<String> strSet){ }  //編譯錯誤  
            public void print(Set<Integer> intSet) { }  //編譯錯誤  
        }  

    很好理解,泛型擦除了麼,此時兩方法相同,編譯器無法區分

6.泛型的應用

  • 集合、類上的型別檢查
  • 通過型別推斷機制,讓方法的使用者不再需要強轉
  • 通過泛型標記,甚至可以在執行期獲取,並例項化相應型別(比如:Gson)
  • 泛型執行期的使用見下篇(Type)

8.參考文章