1. 引言
jdk5.0中引入了Java泛型,目的是減少錯誤,並在型別上新增額外的抽象層。
本文將簡要介紹Java中的泛型、泛型背後的目標以及如何使用泛型來提高程式碼的質量。
2. 為什麼要用泛型?
設想一個場景,我們希望用Java建立一個列表來儲存Integer;程式碼可能會寫成這樣:
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
令人驚訝的是,編譯器會提示最後一行。它不知道返回的資料型別是什麼。因此,編譯器提示需要進行顯式轉換:
Integer i = (Integer) list.iterator.next();
沒有任何約定可以保證列表的返回型別是整數。定義的列表可以包含任何物件。我們只知道我們是通過檢查上下文來檢索列表的。在檢視型別時,它只能保證它是一個Object,因此需要顯式轉換來確保型別是安全的。
這種轉換可能會令人感到聒噪,我們明明知道這個列表中的資料型別是整數。轉換的話,也把我們的程式碼搞得亂七八糟。如果程式設計師在顯式轉換中出錯,則可能會導致丟擲與 型別相關的執行時錯誤 。
如果程式設計師能夠表達他們使用特定型別的意圖,並且編譯器能夠確保這種型別的正確性,那麼這將更加容易。
這就是泛型背後的核心思想。
我們將前面程式碼段的第一行修改為:
List<Integer> list = new LinkedList<>();
通過新增包含型別的菱形運算子<>,我們將此列表的特化範圍縮小到 Integer 型別,即指定將儲存在列表中的型別。編譯器可以在編譯時強制執行該型別。
在較小的程式中,這看起來像是一個微不足道的新增。但是在較大的程式中,這可以增加顯著的健壯性並使程式更易於閱讀。
3. 泛型方法
泛型方法是用單個方法宣告編寫的方法,可以用不同型別的引數呼叫。編譯器將確保所用型別的正確性。以下是泛型方法的一些屬性:
-
泛型方法在方法宣告的返回型別之前有一個型別引數(包裹型別的菱形運算子)
-
型別引數可以有界(邊界將在本文後面解釋)
-
泛型方法可以具有不同的型別引數,這些引數在方法簽名中用逗號分隔
-
泛型方法的方法體與普通方法一樣
定義將陣列轉換為列表的泛型方法的示例:
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
在前面的示例中,方法宣告中的 <T>
表示該方法將處理泛型型別 T。即使方法返回的是void,也需要這樣做。
如上所述,方法可以處理多個泛型型別,在這種情況下,所有泛型型別都必須新增到方法宣告中,例如,如果我們要修改上面的方法來處理型別 T 和型別 G ,應該這樣寫:
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
我們正在傳遞一個函式,該函式將具有T型別元素的陣列轉換為包含G型別元素的列表。例如,將 Integer 轉換為其 String 表示形式:
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
Oracle建議使用大寫字母表示泛型型別,並選擇更具描述性的字母來表示形式型別,例如在Java集合中,T 用於型別,K 表示鍵,V 表示值。
3.1.泛型邊界
如前所述,型別引數可以是有界的。有界意味著“限制”,我們可以限制方法可以接受的型別。
例如,可以指定一個方法接受一個型別及其所有子類(上限)或一個型別所有它的超類(下限)。
要宣告上界型別,我們在型別後面使用關鍵字extends,後跟要使用的上限。例如:
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
這裡使用關鍵字extends表示型別 T 擴充套件類的上限,或者實現介面的上限。
3.2. 多個邊界
型別還可以有多個上界,如下所示:
<T extends Number & Comparable>
如果 T 擴充套件的型別之一是類(即Number),則必須將其放在邊界列表的第一位。否則,將導致編譯時錯誤。
4. 使用萬用字元
萬用字元在Java中用問號“?“ 表示,它們是用來指一種未知的型別。萬用字元在使用泛型時特別有用,可以用作引數型別,但首先要考慮的是一個重要的註釋。
眾所周知,Object是所有Java類的超型別,但是,Object的集合不是任何集合的超型別。(可能有點繞,大家好好細品一下)
例如,List<Object>
不是 List<String>
的超型別,將List<Object>
型別的變數賦值給List<String>
型別的變數將導致編譯器錯誤。
這是為了防止在將異構型別新增到同一集合時可能發生的衝突。
相同的規則適用於型別及其子型別的任何集合。看看這個例子:
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
如果我們設想一個子型別Building,例項House,那麼我們不能將此方法與House列表一起使用,即使House是Building的子型別。如果需要將此方法與型別構建及其所有子型別一起使用,則有界萬用字元可以實現以下功能:
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
現在,這個方法可以處理Building型別及其所有子型別。這稱為上界萬用字元,其中型別Building是上界。
萬用字元也可以使用下限指定,其中未知型別必須是指定型別的超型別。可以使用super關鍵字後跟特定型別來指定下限,例如,<? super T>
表示未知型別,它是 T(=T及其所有父類)的超類。
5. 型別擦除
泛型被新增到Java中以確保型別安全,並確保泛型不會在執行時造成開銷,編譯器在編譯時對泛型應用一個名為type erasure的程式。
型別擦除刪除所有型別引數,並將其替換為它們的邊界,如果型別引數是無界的,則替換為Object。因此,編譯後的位元組碼只包含普通的類、介面和方法,從而確保不會生成新的型別。在編譯時對Object型別也應用了正確的強制轉換。
以下是型別擦除的一個示例:
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
使用型別擦除,無界型別T將替換為Object,如下所示:
// for illustration
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
// which in practice results in
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
如果型別是有界的,則在編譯時該型別將替換為繫結:
public <T extends Building> void genericMethod(T t) {
...
}
編譯後會發生變化:
public void genericMethod(Building t) {
...
}
6. 泛型和原始資料型別
Java中泛型的一個限制是型別引數不能是基本型別
例如,以下內容無法編譯:
List<int> list = new ArrayList<>();
list.add(17);
為了理解原始資料型別為什麼不起作用,只需記住 泛型是編譯時特性,這意味著型別將會被擦除,所有泛型型別都實現為 Object 類。
舉一個例子,讓我們看看列表的 add 方法:
List<Integer> list = new ArrayList<>();
list.add(17);
add 方法的宣告如下:
boolean add(E e);
並將被編譯為:
boolean add(Object e);
因此,型別引數必須可轉換為Object。由於基本型別不繼承自 Object,所以不能將它們用作型別引數
但是,Java為它們提供了裝箱型別,以及自動裝箱和自動拆箱:
Integer a = 17;
int b = a;
因此,如果我們想建立一個可以儲存整數的列表,我們可以使用包裝器:
List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);
編譯後的程式碼相當於:
List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();
Java的未來版本可能允許泛型使用原始資料型別。Valhalla 工程旨在改進處理泛型的方式。其思想是實現JEP 218中描述的泛型專門化.
**7. 總結 **
Java泛型是對Java語言的一個強大的補充,因為它使程式設計師的工作更容易,也更不容易出錯。泛型在編譯時強制執行型別正確性,並且,最重要的是,能夠實現泛型演算法,而不會給我們的應用程式帶來任何額外的開銷。
如果你覺得文章還不錯,記得關注公眾號: 鍋外的大佬
劉一手的部落格