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語言的一個強大的補充,因為它使程式員的作業更容易,也更不容易出錯,泛型在編譯時強制執行型別正確性,并且,最重要的是,能夠實作泛型演算法,而不會給我們的應用程式帶來任何額外的開銷,
如果你覺得文章還不錯,記得關注公眾號: 鍋外的大佬
劉一手的博客
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/211786.html
標籤:其他
