1. 引言
jdk5.0中引入了Java泛型番甩,目的是減少錯(cuò)誤侵贵,并在類型上添加額外的抽象層。
本文將簡要介紹Java中的泛型缘薛、泛型背后的目標(biāo)以及如何使用泛型來提高代碼的質(zhì)量窍育。
2. 為什么要用泛型?
設(shè)想一個(gè)場景宴胧,我們希望用Java創(chuàng)建一個(gè)列表來存儲Integer漱抓;代碼可能會寫成這樣:
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
令人驚訝的是,編譯器會提示最后一行恕齐。它不知道返回的數(shù)據(jù)類型是什么乞娄。因此,編譯器提示需要進(jìn)行顯式轉(zhuǎn)換:
Integer i = (Integer) list.iterator.next();
沒有任何約定可以保證列表的返回類型是整數(shù)。定義的列表可以包含任何對象补胚。我們只知道我們是通過檢查上下文來檢索列表的码耐。在查看類型時(shí),它只能保證它是一個(gè)Object溶其,因此需要顯式轉(zhuǎn)換來確保類型是安全的骚腥。
這種轉(zhuǎn)換可能會令人感到聒噪,我們明明知道這個(gè)列表中的數(shù)據(jù)類型是整數(shù)瓶逃。轉(zhuǎn)換的話束铭,也把我們的代碼搞得亂七八糟。如果程序員在顯式轉(zhuǎn)換中出錯(cuò)厢绝,則可能會導(dǎo)致拋出與 類型相關(guān)的運(yùn)行時(shí)錯(cuò)誤 契沫。
如果程序員能夠表達(dá)他們使用特定類型的意圖,并且編譯器能夠確保這種類型的正確性昔汉,那么這將更加容易懈万。
這就是泛型背后的核心思想。
我們將前面代碼段的第一行修改為:
List<Integer> list = new LinkedList<>();
通過添加包含類型的菱形運(yùn)算符<>靶病,我們將此列表的特化范圍縮小到 Integer 類型会通,即指定將保存在列表中的類型。編譯器可以在編譯時(shí)強(qiáng)制執(zhí)行該類型娄周。
在較小的程序中涕侈,這看起來像是一個(gè)微不足道的添加。但是在較大的程序中煤辨,這可以增加顯著的健壯性并使程序更易于閱讀裳涛。
3. 泛型方法
泛型方法是用單個(gè)方法聲明編寫的方法,可以用不同類型的參數(shù)調(diào)用众辨。編譯器將確保所用類型的正確性端三。以下是泛型方法的一些屬性:
泛型方法在方法聲明的返回類型之前有一個(gè)類型參數(shù)(包裹類型的菱形運(yùn)算符)
類型參數(shù)可以有界(邊界將在本文后面解釋)
泛型方法可以具有不同的類型參數(shù),這些參數(shù)在方法簽名中用逗號分隔
泛型方法的方法體與普通方法一樣
定義將數(shù)組轉(zhuǎn)換為列表的泛型方法的示例:
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
在前面的示例中泻轰,方法聲明中的<T>
表示該方法將處理泛型類型 T技肩。即使方法返回的是void,也需要這樣做浮声。
如上所述虚婿,方法可以處理多個(gè)泛型類型,在這種情況下泳挥,所有泛型類型都必須添加到方法聲明中然痊,例如,如果我們要修改上面的方法來處理類型 T 和類型 G 屉符,應(yīng)該這樣寫:
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
我們正在傳遞一個(gè)函數(shù)剧浸,該函數(shù)將具有T類型元素的數(shù)組轉(zhuǎn)換為包含G類型元素的列表锹引。例如,將 Integer 轉(zhuǎn)換為其 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.泛型邊界
如前所述冯吓,類型參數(shù)可以是有界的。有界意味著“限制”组贺,我們可以限制方法可以接受的類型凸舵。
例如,可以指定一個(gè)方法接受一個(gè)類型及其所有子類(上限)或一個(gè)類型所有它的超類(下限)失尖。
要聲明上界類型啊奄,我們在類型后面使用關(guān)鍵字extends,后跟要使用的上限雹仿。例如:
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
這里使用關(guān)鍵字extends表示類型 T 擴(kuò)展類的上限增热,或者實(shí)現(xiàn)接口的上限。
3.2. 多個(gè)邊界
類型還可以有多個(gè)上界胧辽,如下所示:
<T extends Number & Comparable>
如果 T 擴(kuò)展的類型之一是類(即Number),則必須將其放在邊界列表的第一位公黑。否則邑商,將導(dǎo)致編譯時(shí)錯(cuò)誤。
4. 使用通配符
通配符在Java中用問號“凡蚜?“ 表示人断,它們是用來指一種未知的類型。通配符在使用泛型時(shí)特別有用朝蜘,可以用作參數(shù)類型恶迈,但首先要考慮的是一個(gè)重要的注釋。
眾所周知谱醇,Object是所有Java類的超類型暇仲,但是,Object的集合不是任何集合的超類型副渴。(可能有點(diǎn)繞奈附,大家好好細(xì)品一下)
例如,List<Object>
不是 List<String>
的超類型煮剧,將List<Object>
類型的變量賦值給List<String>
類型的變量將導(dǎo)致編譯器錯(cuò)誤斥滤。
這是為了防止在將異構(gòu)類型添加到同一集合時(shí)可能發(fā)生的沖突将鸵。
相同的規(guī)則適用于類型及其子類型的任何集合∮悠模看看這個(gè)例子:
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
如果我們設(shè)想一個(gè)子類型Building顶掉,實(shí)例House,那么我們不能將此方法與House列表一起使用挑胸,即使House是Building的子類型一喘。如果需要將此方法與類型構(gòu)建及其所有子類型一起使用,則有界通配符可以實(shí)現(xiàn)以下功能:
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
現(xiàn)在嗜暴,這個(gè)方法可以處理Building類型及其所有子類型凸克。這稱為上界通配符,其中類型Building是上界闷沥。
通配符也可以使用下限指定萎战,其中未知類型必須是指定類型的超類型∮咛樱可以使用super關(guān)鍵字后跟特定類型來指定下限蚂维,例如,<? super T>
表示未知類型路狮,它是 T(=T及其所有父類)的超類虫啥。
5. 類型擦除
泛型被添加到Java中以確保類型安全,并確保泛型不會在運(yùn)行時(shí)造成開銷奄妨,編譯器在編譯時(shí)對泛型應(yīng)用一個(gè)名為type erasure的進(jìn)程涂籽。
類型擦除刪除所有類型參數(shù),并將其替換為它們的邊界砸抛,如果類型參數(shù)是無界的评雌,則替換為Object。因此直焙,編譯后的字節(jié)碼只包含普通的類景东、接口和方法,從而確保不會生成新的類型奔誓。在編譯時(shí)對Object類型也應(yīng)用了正確的強(qiáng)制轉(zhuǎn)換斤吐。
以下是類型擦除的一個(gè)示例:
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());
}
如果類型是有界的和措,則在編譯時(shí)該類型將替換為綁定:
public <T extends Building> void genericMethod(T t) {
...
}
編譯后會發(fā)生變化:
public void genericMethod(Building t) {
...
}
6. 泛型和原始數(shù)據(jù)類型
Java中泛型的一個(gè)限制是類型參數(shù)不能是基本類型
例如,以下內(nèi)容無法編譯:
List<int> list = new ArrayList<>();
list.add(17);
為了理解原始數(shù)據(jù)類型為什么不起作用杯聚,只需記住 泛型是編譯時(shí)特性臼婆,這意味著類型將會被擦除,所有泛型類型都實(shí)現(xiàn)為 Object 類幌绍。
舉一個(gè)例子颁褂,讓我們看看列表的 add 方法:
List<Integer> list = new ArrayList<>();
list.add(17);
add 方法的聲明如下:
boolean add(E e);
并將被編譯為:
boolean add(Object e);
因此故响,類型參數(shù)必須可轉(zhuǎn)換為Object。由于基本類型不繼承自 Object颁独,所以不能將它們用作類型參數(shù)
但是彩届,Java為它們提供了裝箱類型,以及自動裝箱和自動拆箱:
Integer a = 17;
int b = a;
因此誓酒,如果我們想創(chuàng)建一個(gè)可以保存整數(shù)的列表樟蠕,我們可以使用包裝器:
List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);
編譯后的代碼相當(dāng)于:
List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();
Java的未來版本可能允許泛型使用原始數(shù)據(jù)類型。Valhalla 工程旨在改進(jìn)處理泛型的方式靠柑。其思想是實(shí)現(xiàn)JEP 218中描述的泛型專門化.
**7. 總結(jié) **
Java泛型是對Java語言的一個(gè)強(qiáng)大的補(bǔ)充寨辩,因?yàn)樗钩绦騿T的工作更容易,也更不容易出錯(cuò)歼冰。泛型在編譯時(shí)強(qiáng)制執(zhí)行類型正確性靡狞,并且,最重要的是隔嫡,能夠?qū)崿F(xiàn)泛型算法甸怕,而不會給我們的應(yīng)用程序帶來任何額外的開銷。
如果你覺得文章還不錯(cuò)腮恩,記得關(guān)注公眾號: 鍋外的大佬
劉一手的博客