年后跑路第一戰(zhàn),從 Java 泛型學(xué)起!
文末本文轉(zhuǎn)載自微信公眾號(hào)「愛寫B(tài)ug的麥洛」,作者麥洛 。轉(zhuǎn)載本文請(qǐng)聯(lián)系愛寫B(tài)ug的麥洛公眾號(hào)。
概述
大家好,我是麥洛,今天來復(fù)習(xí)一下泛型。JDK 5.0 引入了 Java 泛型,允許設(shè)計(jì)者詳細(xì)地描述變量和方法的類型要如何變化,使得代碼具有更好的可讀性。本文章是對(duì) Java 中泛型的快速介紹,包含泛型背后的目標(biāo)以及使用泛型如何提高我們代碼的質(zhì)量。
為什么要引入泛型?
在沒有泛型的背景下,讓我們想象一個(gè)場(chǎng)景,我們要在 Java 中創(chuàng)建一個(gè)List來存儲(chǔ)Integer。
代碼如下:
- List list = new LinkedList();
- list.add(new Integer(1));
- Integer i = list.iterator().next();
果不其然,IDEA會(huì)直接提醒需要強(qiáng)制轉(zhuǎn)換。
我們對(duì)代碼進(jìn)行修改,如下所示:
- Integer i = (Integer) list.iterator.next();
在沒有泛型的前提下,定義的List可以保存任何對(duì)象,當(dāng)我們遍歷時(shí)候,根據(jù)上下文進(jìn)行判斷,只能保證它是一個(gè)Object,所以需要我們顯示轉(zhuǎn)換。
我們知道List中的數(shù)據(jù)類型是Integer,可以直接強(qiáng)制轉(zhuǎn)換,如果我們不知道或者強(qiáng)制轉(zhuǎn)換時(shí)候?qū)戝e(cuò)類型,就會(huì)導(dǎo)致報(bào)錯(cuò),一場(chǎng)災(zāi)難就這樣發(fā)生了。
這時(shí)候,就有人想了,我能不能在使用List時(shí)候就指定保存的類型,編譯階段來幫我保證類型的正確性,那就可以完全避免讓人討厭的強(qiáng)制轉(zhuǎn)換,所以,泛型就因運(yùn)而生了。
讓我們修改前面代碼片段的第一行:
- List<Integer> list = new LinkedList<>();
通過添加包含類型的菱形運(yùn)算符 <>,我們將List能保存的類型限制到只有Integer類型,編譯器可以在編譯時(shí)強(qiáng)制執(zhí)行類型。
泛型方法
對(duì)于泛型方法,我們可以用不同類型的參數(shù)調(diào)用它們。編譯器將確保我們使用的任何類型的正確性。
泛型方法屬性:
- 泛型方法在方法聲明的返回類型之前有一個(gè)類型參數(shù)(包含類型的菱形運(yùn)算符)。
- 類型參數(shù)可以是有界的(我們將在本文后面解釋邊界)。
- 泛型方法可以在方法簽名中具有用逗號(hào)分隔的不同類型參數(shù)。
- 泛型方法的方法體就像普通方法一樣。
這是定義將數(shù)組轉(zhuǎn)換為L(zhǎng)ist的泛型方法的示例:
- public <T> List<T> fromArrayToList(T[] a) {
- return Arrays.stream(a).collect(Collectors.toList());
- }
方法簽名中的
如前所述,該方法可以處理多個(gè)泛型類型。在這種情況下,我們必須將所有泛型類型添加到方法簽名中。
以下是我們?nèi)绾涡薷纳鲜龇椒ㄒ蕴幚眍愋蚑和類型G:
- 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類型元素的列表。
一個(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"));
- }
請(qǐng)注意,Oracle 建議使用大寫字母來表示泛型類型,并選擇更具描述性的字母來表示正式類型。在 Java 集合中,我們使用T表示類型,K表示鍵,V表示值。
有界泛型
類型參數(shù)可以有界,我們可以限制方法接受的類型。例如,我們可以指定一個(gè)方法接受一個(gè)類型及其所有子類(上限)或一個(gè)類型及其所有超類(下限)。要聲明上界類型,我們?cè)陬愋秃笫褂藐P(guān)鍵字extends,要聲明下界類型,我們?cè)陬愋秃笫褂藐P(guān)鍵字super。
例子:
- public <T extends Number> List<T> fromArrayToList(T[] a) {
- ...
- }
我們?cè)谶@里使用關(guān)鍵字 extends 表示類型 T 在類的情況下擴(kuò)展上限或在接口的情況下實(shí)現(xiàn)上限。
多重邊界
一個(gè)類型也可以有多個(gè)上限:
如果T擴(kuò)展的類型之一是一個(gè)類(例如Number),我們必須將它放在邊界列表中的第一個(gè)。否則會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。
在泛型中使用通配符
在Java中,通配符由?表示,我們使用它們來指代未知類型。通配符對(duì)泛型特別有用,可以用作參數(shù)類型。
首先,我們知道Object是所有 Java 類的超類。但是,Object的集合不是任何集合的超類型。所以,一個(gè)List 不是List
例子:
- public static void paintAllBuildings(List<Building> buildings) {
- buildings.forEach(Building::paint);
- }
假如現(xiàn)在有一個(gè)Building 的子類型,叫House,我們不能將這個(gè)方法用于 House 的列表,即使 House 是 Building 的一個(gè)子類型。
如果我們需要將此方法與類型 Building 及其所有子類型一起使用,則有界通配符可以發(fā)揮作用:
- public static void paintAllBuildings(List<? extends Building> buildings) {
- ...
- }
現(xiàn)在此方法將適用于類型 Building 及其所有子類型。這稱為上限通配符,其中類型 Building 是上限。
我們還可以指定具有下限的通配符,其中未知類型必須是指定類型的超類型。可以使用 super 關(guān)鍵字后跟特定類型來指定下限。例如, 表示未知類型,它是 T 的超類(= T 及其所有父類)。
類型擦除
Java 中添加了泛型以確保類型安全。并且為了確保泛型不會(huì)在運(yùn)行時(shí)造成開銷,編譯器在編譯時(shí)對(duì)泛型應(yīng)用了一個(gè)稱為類型擦除的過程。
如果類型參數(shù)是無界的,則類型擦除會(huì)刪除所有類型參數(shù)并用它們的邊界或Object替換它們。這樣,編譯后的字節(jié)碼只包含正常的類、接口和方法,確保不會(huì)產(chǎn)生新的類型。在編譯時(shí)也將正確的轉(zhuǎn)換應(yīng)用于 Object 類型。
這是類型擦除的示例:
- public <T> List<T> genericMethod(List<T> list) {
- return list.stream().collect(Collectors.toList());
- }
使用類型擦除,無界類型T被替換為Object:
- public List<Object> withErasure(List<Object> list) {
- return list.stream().collect(Collectors.toList());
- }
- public List withErasure(List list) {
- return list.stream().collect(Collectors.toList());
- }
如果類型是有界的,則在編譯時(shí)該類型將被邊界替換:
- public <T extends Building> void genericMethod(T t) {
- ...
- }
編譯后:
- public void genericMethod(Building t) {
- ...
- }
泛型和原始數(shù)據(jù)類型
Java 中泛型的一個(gè)限制是類型參數(shù)不能是基本類型。
例如,以下不能編譯:
- List<int> list = new ArrayList<>();
- list.add(17);
要理解基本類型為什么不起作用,讓我們記住泛型是一個(gè)編譯時(shí)特性,這意味著類型參數(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。由于基本類型不擴(kuò)展Object,我們不能將它們用作類型參數(shù)。
然而,Java 為原語提供了裝箱類型,以及自動(dòng)裝箱和拆箱來解包它們:
- Integer a = 17;
- int b = a;
所以,如果我們想創(chuàng)建一個(gè)可以容納整數(shù)的列表,我們可以使用這個(gè)包裝器:
- List<Integer> list = new ArrayList<>();
- list.add(17);
- int first = list.get(0);
編譯后的代碼將等效于以下內(nèi)容:
- List list = new ArrayList<>();
- list.add(Integer.valueOf(17));
- int first = ((Integer) list.get(0)).intValue();
結(jié)論
Java 泛型是對(duì) Java 語言的強(qiáng)大補(bǔ)充,因?yàn)樗钩绦騿T的工作更輕松且不易出錯(cuò)。泛型在編譯時(shí)強(qiáng)制類型正確,最重要的是,可以實(shí)現(xiàn)泛型算法而不會(huì)對(duì)我們的應(yīng)用程序造成任何額外開銷。