1. 程式人生 > >Java泛型與型別擦除

Java泛型與型別擦除

一、什麼是泛型;

泛型的本質是 引數化型別,也就是說 將所操作的資料型別 指定為一個引數,在不建立新的型別的情況下,通過泛型指定的不同型別,來控制形參具體限制的型別。類似於方法中的變數引數,此時型別也定義成引數形式,然後在使用的過程中,指定具體的型別。

這種引數型別可以在類、介面、方法中使用,分別稱為泛型類、泛型介面、泛型方法。

二、泛型的好處:

沒有泛型的情況的下,通過對型別Object的引用來實現引數的“任意化”,“任意化”帶來的缺點是要做顯式的強制型別轉換,而這種轉換是要求開發者對實際引數型別可以預知的情況下進行的。使用泛型有如下好處:

(1)編譯時的強型別檢查,更好的型別安全性:

對於強制型別轉換錯誤的情況,編譯器可能不提示錯誤,在執行的時候才出現異常,這是一個安全隱患。泛型的好處是在編譯時期檢查型別安全,並能捕捉型別不匹配的錯誤,避免執行時丟擲型別轉化異常ClassCastException,將執行時錯誤提前到編譯時錯誤,消除安全隱患。

(2)消除顯式的型別強制轉換與更好的可讀性:

泛型所有的強制轉換都是自動和隱式的,可以提高程式碼的重用率,省去繁瑣的強制型別轉換,再加上明確的型別資訊,程式碼的可讀性也會更好。

三、Java類庫中的泛型有那些?泛型的用途?

(1)泛型類:最常見的用途就是容器類,通過泛型可以完成對一組類的操作對外開放相同的介面。所有的標準集合介面都是泛型化的---Collection<V>、List<V>、Set<V> 和 Map<K,V>。

(2)泛型介面:類似地,集合介面的實現都是用相同型別引數泛型化的,所以HashMap<K,V> 實現 Map<K,V> 等都是泛型的,Comparable和Comparator介面也是泛型的。

除了集合類之外,Java 類庫中還有幾個其他的類也充當值的容器。這些類包括 WeakReference、SoftReference 和 ThreadLocal。

(3)泛型方法:要定義泛型方法,只需將泛型引數列表置於返回值之前。

靜態方法上的泛型:靜態方法無法訪問類上定義的泛型。如果靜態方法操作的引用資料型別不確定的時候,必須要將泛型定義在方法上。如下:

public static<Q> void function(Q t) {
  System.out.println("function:"+t);
}

四、泛型的上界下界:

<?extends T> 表示型別的上界,引數化型別可能是T 或者是 T的子類;

<? super T> 表示型別的下界,引數化型別是此T型別的超型別,直至object;

上界什麼時候用:往集合中新增元素時,既可以新增T型別物件,又可以新增T的子型別物件。為什麼?因為存的時候,T型別既可以接收T類物件,又可以接收T的子型別物件。

下界什麼時候用:當從集合中獲取元素進行操作的時候,可以用當前元素的型別接收,也可以用當前元素的父型別接收。

五、Java泛型的實現方法--型別擦除:

Java泛型的實現是靠型別擦除技術實現的,型別擦除是在編譯期完成的,也就是在編譯期,編譯器會將泛型的型別引數都擦除成它的限定型別,如果沒有則擦除為object型別,之後在獲取的時候再強制型別轉換為對應的型別,在生成的Java位元組碼中是不包含泛型中的型別資訊的,即執行期間並沒有泛型的任何資訊。

六、型別擦除詳解:

(1)在使用泛型的時候,雖然傳入了不同的泛型實參,但並沒有真正意義上生成不同的型別,傳入不同泛型實參的泛型類在記憶體中只有一個,即還是原來的最基本的型別;泛型只在編譯代階段有效,在編譯過程中,對於正確檢驗泛型結果後,會將泛型的相關資訊擦除,並且在物件進入和離開方法的邊界處新增型別檢查和型別轉化的方法,也就是說,成功編譯後的class檔案是不包含任何泛型資訊的。

(2)因此,泛型型別在邏輯上可以看成是多個不同的型別,但實際上都是相同的基本型別。型別引數在執行中並不存在,這意味著他們不會新增任何的時間和空間上的負擔;但是,這也意味著不能依靠他們進行型別轉換。

舉兩個例子說明一下型別擦除:

6.1、型別擦除:

public class Test4 {  
      public static void main(String[] args) {  
          ArrayList<String> arrayList1=new ArrayList<String>();  
          arrayList1.add("abc");  
          ArrayList<Integer> arrayList2=new ArrayList<Integer>();  
          arrayList2.add(123);  
         System.out.println(arrayList1.getClass()==arrayList2.getClass());  //true
     }  
  }  

在這個例子中,我們定義了兩個ArrayList陣列,不過一個是ArrayList<String>泛型型別,只能儲存字串。一個是ArrayList<Integer>泛型型別,只能儲存整形。最後,我們通過arrayList1物件和arrayList2物件的getClass方法獲取它們的類的資訊,最後發現結果為true。說明泛型型別String和Integer都被擦除掉了,只剩下了原始型別。

6.2、轉型和instanceof :

//泛型類被所有例項(instances)共享的另一個暗示是檢查一個特定型別的泛型類是沒有意義的。
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // 非法

類似的,如下的型別轉換
Collection<String> cstr = (Collection<String>) cs;
得到一個unchecked warning,因為執行時環境不會為你作這樣的檢查。

七、型別擦除帶來的問題:

八、關於泛型的其他一些小細節:

1、可以建立泛型陣列嗎?相應的應用場景怎麼處理?

     不能建立泛型陣列。一般的解決方案是任何想要建立泛型陣列的地方都使用ArrayList?

2、可以將基本型別作為泛型引數嗎?

     泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別(基本資料型別)。

3、什麼時候用泛型?

     當介面、類及方法中的操作的引用資料型別不確定的時候,以前用的Object來進行擴充套件的,現在可以用泛型來表示。這樣可以避免強轉的麻煩,而且將執行問題轉移到的編譯時期。

4、泛型的細節:

(1)泛型實際代表什麼型別,取決於呼叫者傳入的型別,如果沒傳,預設是Object型別;

(2)使用帶泛型的類建立物件時,等式兩邊指定的泛型型別必須一致。

        原因:編譯器檢查物件呼叫方法時只看變數,然而程式在執行期間呼叫方法時就要考慮物件具體型別了。

(3)等式兩邊可以在任意一邊使用泛型,在另一邊不使用(考慮向後相容);

ArrayList<String>al = new ArrayList<Object>();  //錯
//要保證左右兩邊的泛型具體型別一致就可以了,這樣不容易出錯。
ArrayList<?extends Object> al = new ArrayList<String>();
al.add("aa");  //錯
//因為集合具體物件中既可儲存String,也可以儲存Object的其他子類,所以新增具體的型別物件不合適,型別檢查會出現安全問題。

其他參考部落格: