Java 便利貼之泛型

為什麼要使用泛型?
Java 語言以嚴謹著稱,但在設計的時候忽略了「泛型」這個重要的概念,增加了使用者的責任,使用者需要記住每個元素的型別,同時還得強制轉型,編譯器無法幫忙,在執行時才會丟擲 Class Cast 異常。
泛型是什麼?
Java 5 以後,引入了引數化型別(parameterized type),Java 的引數化型別被稱為泛型(Generic)。所謂泛型,就是允許在定義類、介面、方法時使用型別形參,這個型別形參(或叫泛型)將在宣告變數、建立物件、呼叫方法時動態地指定(即傳入實際的型別引數,也可稱為型別實參)。
深入泛型

上述程式碼中,向 List 中添加了一個字串和整數,看起來沒問題,但是在使用的時候,我們必須要知道第一個元素是字串型別,第二個元素是整型,並且還得強制轉換,否則執行時會報錯。這大大增加了使用者的責任,編譯器無法幫忙,只有在執行時才會丟擲 Class Cast 異常。
那麼怎麼解決這個問題呢?答案就是使用 泛型 。

,將來可以傳入任意型別,如 Integer、String 等。同時我們也可以看到上面泛型的定義也比較簡單,除了尖括號中的內容——這就是泛型的實質:允許在定義類、介面和方法時宣告泛型形參,泛型形參在整個介面、類體內可當成型別使用,幾乎所有可使用普通型別的地方都可以使用這種泛型形參。
可是說來輕巧,那麼泛型又是如何實現的呢?
每次例項化一個泛型/模板類都會生成一個新的類。例如模板類是 List,用 int、double、string、Employee 分別去例項化,那麼在編譯的時候,就會生成 4 個新類,如 List_int、List_double、List_string、List_Employee。
PS包含泛型宣告的型別可以在定義變數、建立物件時傳入一個型別實參。從而可以動態地生成無數多個邏輯上的子類,但這種子類在物理上是不存在的。所以即使生成了很多新類,但也不必擔心繫統會膨脹爆炸。
因為 Java 中對此使用的是擦除法,簡單來說就是一個引數化的型別經過擦除後會去除引數,如 ArrayList<T> 會被擦除為 ArrayList。我們傳入的 String、Integer 等非但沒有消失,反而都變成了 Object,如 ArrayList<Integer>被擦除成了原始的 ArrayList。
但是問題又來了,我們使用泛型是為了避開強制轉型而減少使用者的責任,而現在型別被擦除了,都變成了 Object,本來可以寫成 Integer i = list1.get(0); ,但現在被擦除成了 Object,我們該怎麼處理呢?
其實很簡單,就是在編譯的時候做一點手腳,加一個自動的轉型,如 Integer i = (Integer)list1.get(0); 。
泛型方法
前面說的是泛型類,但是對於一些靜態方法來說,怎麼將其變成泛型呢?其實很簡單,就是將泛型型別 <T> 移到方法上去。如下例子:

很顯然,這個靜態方法是求最大值的,也就是說需要對 List 中的元素比較大小。但是如果傳入的 T 沒有實現 Comparable 介面,那麼就沒法比較大小。所以我們需要做一個型別的限制,限制傳入的型別 T 必須是 Comparable 的子類,否則編譯器報錯。如下:

不過除了使用 extends 關鍵字之外,Java 泛型的限制符還支援 super,實際上為了更靈活使用,上面的 Comparable<T> 應該寫成 Comparable<? super T>。
泛型和繼承

類圖

程式碼清單
從上述類圖和程式碼中,我們發現當傳遞一個 ArrayList<Apple>時編譯會出錯,可是這到底是怎麼回事兒呢? print() 方法能接收的引數不是 ArrayList<Fruit> 嗎?
其實這裡存在一個誤區,就是 Apple 雖然是 Fruit 的子類,但是 ArrayList<Apple> 卻不是 ArrayList<Fruit> 的子類,實際上它們之間是沒有任何關係的,不能執行轉型操作,所以在呼叫 print() 方法的時候會報錯。
【注】如果 Foo 是 Bar 的一個子型別(子類或者子介面),而 G 是具有泛型宣告的類或介面,G<Foo> 並不是 G<Bar> 的子型別。
可是為什麼不能將 ArrayList<Apple> 轉換成 ArrayList<Fruit> 呢?那是因為如果可以這麼做,那麼我們不但可以向這個 Lsit 中加入 Apple,還能加入 Orange,泛型就被破壞了。
那麼對於這個問題要怎麼解決呢?可以通過引用萬用字元的方式加以解決,可以將方法的輸入引數修改如下:

也就是說,傳進來的引數只要是 Fruit 或者 Fruit 的子類都可以,這樣一來就可以接收 ArrayList<Fruit>、ArrayList<Apple> 和 ArrayList<Orange> 這樣的引數了。但是有一點需要注意的是,這種帶萬用字元的 List 僅表示它是各種泛型 List 的父類,並不能向其中新增元素,比如如下程式碼就會引起編譯錯誤。這是因為程式無法確定集合是哪種型別,所以只能對集合進行讀操作,而不能向其中新增資料。

本文參考
《碼農翻身》
《瘋狂Java講義》
《一文讀懂Java泛型中的萬用字元?》(趣談程式設計)