愉快地學Java語言:第十四章 泛型

導讀

本文適合Java入門,不太適合Java中高階軟體工程師。本文以《Java核心技術基礎知識卷I》第10版為藍本,採用不斷提出問題,然後解答問題的方式來講述。本篇文章只是這個系列中的一篇,如果你喜歡這種講解方式,或者覺得從中能學到知識,可以關注我,以便查閱本系列其他文章。

讓我們開始愉快地學習Java語言吧!

愉快地學Java語言:第十四章 泛型

1

型別引數的魅力

為什麼要使用範型呢?

型別引數使得程式具有更好的可讀性和安全性。沒有泛型之前,我們要針對每一種型別程式設計,哪怕處理他們的邏輯都是相同的。有了泛型以後,就相當為我們提供了一個模板,建立類的時候只考慮共同的、通用的部分就可以,等到具體使用時在分配具體型別。

2基本概念

泛型類

:具有一個或多個型別變數的類。泛型類可看作普通類的工廠。

型別引數

:用<>中的字母表示形式泛型型別,也稱為

形式範型型別

怎麼表示型別引數呢?

在Java API中,使用變數E表示集合的元素型別,K和V分別表示表的關鍵字與值的型別。T、 U、S表示“任意型別”。所以我們自定義範型型別的時候也遵守這個規則。

實際具體型別:指使用泛型時替換型別引數的具體型別。

如果我想對型別引數加以約束該怎麼辦呢?

使用型別引數限定:

其中,T是BoundingType的子型別,BoundingType可以是類,也可以是介面,我們稱之為限定型別。

一個型別引數或萬用字元可以有多個BoundingType(限定型別),用逗號分隔型別引數,用&分隔BoundingType。

多個BoundingType中至多有一個類,若多個BoundingType中包含類,那麼必須位於BoundingType列表中的第一個。

例項化泛型型別:用具體的型別替換型別引數。

型別引數可以是基本資料型別嗎?

型別引數必須是引用型別。

來看看Java API中定義的泛型:

ArrayList,它使用E表示集和變數,它是抽象類AbstractList的子類,並且實現了範型介面List

愉快地學Java語言:第十四章 泛型

下面是HashMap,它使用K和V分別表示表的關鍵字與值的型別。同時它是AbstractMap類的子類,並且它實現了Map這個範型介面。

愉快地學Java語言:第十四章 泛型

3泛型方法

帶有型別引數的方法被稱為範型方法。

型別引數放在修飾符的後面,返回值型別的前面。

泛型方法可以定義在普通類中,也可以定義在泛型類中。

當呼叫一個泛型方法時,在方法名前的尖括號中放入具體的型別。通常,省略尖括號及其中的具體型別。

下面這段程式碼使用了Java API中的ArrayList,注意,第一個實際具體型別是Integer,但是我們新增的卻是1,即基本型別,這裡發生了自動轉型。

愉快地學Java語言:第十四章 泛型

在普通方法中定義範型方法:

愉快地學Java語言:第十四章 泛型

4型別擦除

虛擬機器中沒有泛型,只有普通的類和方法,因此有了型別擦除機制。編譯器會使用範型資訊編譯程式碼,隨後就會刪除型別引數。

原始型別(raw type)

:刪去型別引數後的泛型型別名。無論何時定義一個泛型型別,都自動提供了一個相應的原始型別。

在型別擦除過程中,泛型型別名後跟的型別引數被刪除,那麼泛型表示式和泛型方法的型別引數要被替換(實際是強制轉型),於是有了下面的兩個概念。

翻譯泛型表示式

:翻譯器會在表示式位元組碼中插入強制型別轉換。

翻譯泛型方法

:當程式呼叫泛型方法時,如果擦除返回型別,編譯器插入強制型別轉換。

為什麼會有原始型別?

因為要後向相容Java早期的版本。

為什麼說原始型別是不安全的?

因為原始型別繞過了通用型別檢查,所以是不安全的。

型別擦除就是刪去形式泛型型別,編譯器將其替換為限定型別,替換規則是什麼呢?

未對型別引數進行限定,就用Object替換。

如果只有一個限定型別,那麼就用這個限定型別替換。

如果有多個限定型別則使用第一個限定型別替換。

5泛型限制

範型雖然好用,但也有一些限制,具體如下:

1)實際具體型別只能是引用型別

2)執行時型別查詢只適用於原始型別,倘若使用 instanceof 會得到一個編譯器錯誤,如果使用強制型別轉換會得到一個警告,getClass方法總是返回原始型別。

讓我們驗證一下:

愉快地學Java語言:第十四章 泛型

愉快地學Java語言:第十四章 泛型

3)不能建立引數化型別的陣列,不過可用於變數宣告。

向下面這樣建立沒有問題,就是有一個警告而已,不過做好還是不要用這麼奇怪的用法了,看著都危險。

愉快地學Java語言:第十四章 泛型

然而加上<>就報錯了。

愉快地學Java語言:第十四章 泛型

使用萬用字元,然後強制型別轉換,不過還是有警告,所以還是別這樣用。

愉快地學Java語言:第十四章 泛型

4)定義一個引數個數可變的方法,並且引數型別為泛型型別引數時會有警告,為了消除警告,使用@SuppressWarnings(“unchecked”)或@SafeVarargs註解。

愉快地學Java語言:第十四章 泛型

5)不能例項化型別引數,不能使用像new T(。。。) 、new T[。。。]或T。class這樣的表示式。

如果還想給方法傳遞一個這種物件,那怎麼辦呢?

還記得講lambda表示式那章嗎?有個概念是構造器引用,用這個就可以解決。

6)不能在靜態域中使用型別引數。

愉快地學Java語言:第十四章 泛型

7)既不能丟擲也不能捕獲泛型類物件。

這句話的含義其實是,泛型類沒法擴充套件Throwable,同時catch子句中不能使用型別變數。

如下圖,定義一個泛型類,想讓它擴充套件自Exception,但是沒辦法辦到。看編譯提示,無法繼承Throwable。

愉快地學Java語言:第十四章 泛型

catch塊中也無法使用型別引數,如下例所示:

愉快地學Java語言:第十四章 泛型

但宣告丟擲一個異常時,可以使用泛型。

愉快地學Java語言:第十四章 泛型

8) 型別擦除後可能引起的衝突

我們要尊受一個規則:要想支援擦除的轉換,就需要強行限制一個類或型別變數不能同時成為兩個介面型別的子類,而這兩個介面是同一介面的不同引數化。

好再我們有編譯器提示我們,如下面

愉快地學Java語言:第十四章 泛型

愉快地學Java語言:第十四章 泛型

檢視錯誤資訊:The interface Comparable cannot be implemented more than once with different arguments: Comparable and Comparable

6泛型類繼承規則

引數化型別是原始型別的子類,可以將引數化型別轉換為一個原始型別。

泛型類可以擴充套件或實現其他的泛型類。

兩個泛型類,只有型別引數具有父子關係,那麼這兩個泛型類不具有父子關係。

7萬用字元型別

為什麼引入萬用字元型別呢?

固定型別的泛型有時用起來也不是很方便。比較常見的就是,若兩個泛型類只有型別引數具有父子關係,那麼這兩個泛型類不具有父子關係。不能將一個類的例項賦給另一個類的例項變數。所以我們尋找更方便的方式,即萬用字元型別。

那麼什麼是子型別限定和超型別限定呢,他們有什麼區別?

子型別限定,即<? extends T>,型別引數被限定為T的子類。

超型別限定,即<? super T>,型別引數被限定為T的父類。

除了上述概念,還有無限定萬用字元,形如:原始型別名<?>,不過不常用。

愉快地學Java語言:第十四章 泛型

讓我們看看報錯資訊:The method add(capture#2-of ?) in the type ArrayList is not applicable for the arguments (Object)

顯然是捕獲的型別和Object不匹配。