官术网_书友最值得收藏!

2.6 泛型

泛型通過(guò)在編譯時(shí)檢測(cè)到更多的代碼Bug,從而使你的代碼更加穩(wěn)定。

2.6.1 泛型的作用

概括地說(shuō),泛型支持類(lèi)型(類(lèi)和接口)在定義類(lèi)、接口和方法時(shí)可以作為參數(shù)。就像在方法聲明中使用的形式參數(shù)一樣,類(lèi)型參數(shù)提供了一種輸入可以不同但代碼可以重用的方式。所不同的是,形式參數(shù)的輸入是值,類(lèi)型參數(shù)輸入的是類(lèi)型。

使用泛型對(duì)比非泛型代碼有很多好處:

1.在編譯時(shí)更強(qiáng)的類(lèi)型檢查

如果代碼違反了類(lèi)型安全,Java編譯器將針對(duì)泛型和問(wèn)題錯(cuò)誤采用強(qiáng)大的類(lèi)型檢查。修正編譯時(shí)的錯(cuò)誤比修正運(yùn)行時(shí)的錯(cuò)誤更加容易。

2.消除了強(qiáng)制類(lèi)型轉(zhuǎn)換

沒(méi)有泛型的代碼片需要強(qiáng)制轉(zhuǎn)化,比如:

當(dāng)重新編寫(xiě)使用泛型時(shí),代碼不需要強(qiáng)轉(zhuǎn):

3.使編程人員能夠?qū)崿F(xiàn)通用算法

通過(guò)使用泛型,程序員可以實(shí)現(xiàn)工作在不同類(lèi)型集合的通用算法,并且可定制、類(lèi)型安全、易于閱讀。

2.6.2 泛型類(lèi)型

泛型類(lèi)型是參數(shù)化類(lèi)型的泛型類(lèi)或接口。下面通過(guò)一個(gè)Box類(lèi)的例子來(lái)說(shuō)明這個(gè)概念。

1.一個(gè)簡(jiǎn)單的Box類(lèi)

觀察下面的例子:

它的方法接受或返回一個(gè)Object,你可以自由地傳入任何你想要的類(lèi)型,只要它不是原始的類(lèi)型之一即可。在編譯時(shí),沒(méi)有辦法驗(yàn)證如何使用這個(gè)類(lèi)。代碼的一部分可以設(shè)置Integer并期望得到Integer ,而代碼的另一部分可能會(huì)由于錯(cuò)誤地傳遞一個(gè)String而導(dǎo)致運(yùn)行錯(cuò)誤。

2.一個(gè)泛型版本的Box類(lèi)

泛型類(lèi)定義語(yǔ)法如下:

類(lèi)型參數(shù)部分用<>包裹,制定了類(lèi)型參數(shù)(或稱(chēng)為類(lèi)型變量)T1、T2、…、Tn。

下面是泛型版本代碼的例子:

可以看到,所有的Object都被T代替了。類(lèi)型變量可以是非基本類(lèi)型的任意類(lèi)型,即任意的類(lèi)、接口、數(shù)組或其他類(lèi)型變量。

這個(gè)技術(shù)同樣適用于泛型接口的創(chuàng)建。

3.類(lèi)型參數(shù)命名規(guī)范

按照慣例,類(lèi)型參數(shù)名稱(chēng)是單個(gè)大寫(xiě)字母,用來(lái)區(qū)別普通的類(lèi)或接口名稱(chēng)。常用的類(lèi)型參數(shù)名稱(chēng)如下:

· E:元素,主要由Java集合(Collections)框架使用。

· K:鍵,主要用于表示映射中的鍵的參數(shù)類(lèi)型。

· V:值,主要用于表示映射中的值的參數(shù)類(lèi)型。

· N:數(shù)字,主要用于表示數(shù)字。

· T:類(lèi)型,主要用于表示第一類(lèi)通用型參數(shù)。

· S:類(lèi)型,主要用于表示第二類(lèi)通用類(lèi)型參數(shù)。

· U:類(lèi)型,主要用于表示第三類(lèi)通用類(lèi)型參數(shù)。

· V:類(lèi)型,主要用于表示第四類(lèi)通用類(lèi)型參數(shù)。

4.調(diào)用和實(shí)例化一個(gè)泛型

從代碼中引用泛型Box類(lèi),必須執(zhí)行一個(gè)泛型調(diào)用,用具體的值(比如Integer)取代T:

   Box<Integer> integerBox;

泛型調(diào)用與普通的方法調(diào)用類(lèi)似,所不同的是傳遞參數(shù)是類(lèi)型參數(shù),在本例中就是傳遞Integer到Box類(lèi)。

Type Parameter和Type Argument的區(qū)別

編碼時(shí),提供type argument的一個(gè)原因是為了創(chuàng)建參數(shù)化類(lèi)型。因此,F(xiàn)oo<T>中的T是一個(gè)type parameter,而Foo<String>中的String是一個(gè)type argument。

與其他變量聲明類(lèi)似,代碼實(shí)際上沒(méi)有創(chuàng)建一個(gè)新的Box對(duì)象。它只是聲明integerBox在讀到Box<Integer>時(shí),保存一個(gè)“Integer的Box”的引用。

泛型的調(diào)用通常被稱(chēng)為一個(gè)參數(shù)化類(lèi)型。

實(shí)例化類(lèi),使用new關(guān)鍵字:

5.菱形(Diamond)

從Java SE 7開(kāi)始,泛型可以使用空的類(lèi)型參數(shù)集<>,只要編譯器能夠確定或推斷該類(lèi)型參數(shù)所需的類(lèi)型參數(shù)即可。這對(duì)尖括號(hào)<>被非正式地稱(chēng)為“菱形(Diamond)”,例如:

6.多類(lèi)型參數(shù)

下面是一個(gè)泛型Pair接口和一個(gè)泛型OrderedPair:

創(chuàng)建兩個(gè)OrderedPair實(shí)例:

在代碼“new OrderedPair<String, Integer>”中,實(shí)例K作為一個(gè)String、V作為一個(gè)Integer。因此,OrderedPair構(gòu)造函數(shù)的參數(shù)類(lèi)型是String和Integer。由于有自動(dòng)裝箱機(jī)制,因此可以有效地傳遞一個(gè)String和int到這個(gè)類(lèi)。

可以使用菱形(diamond)來(lái)簡(jiǎn)化代碼:

7.參數(shù)化類(lèi)型

也可以用參數(shù)化類(lèi)型(例如,List<String>的)來(lái)替換類(lèi)型參數(shù)(即K或V)。例如,使用OrderedPair<K,V>:

8.原生類(lèi)型

原生類(lèi)型是沒(méi)有類(lèi)型參數(shù)的泛型類(lèi)和泛型接口,如泛型Box類(lèi):

要?jiǎng)?chuàng)建參數(shù)化類(lèi)型的Box<T>,需要為形式類(lèi)型參數(shù)T提供實(shí)際的類(lèi)型參數(shù):

如果想省略實(shí)際的類(lèi)型參數(shù),就需要?jiǎng)?chuàng)建一個(gè)Box<T>的原生類(lèi)型:

因此,Box是泛型Box<T>的原生類(lèi)型。但是,非泛型的類(lèi)或接口類(lèi)型不是原始類(lèi)型。

JDK為了保證向后兼容,允許將參數(shù)化類(lèi)型分配給原始類(lèi)型:

如果將原始類(lèi)型與參數(shù)化類(lèi)型進(jìn)行管理,就會(huì)得到警告:

如果使用原始類(lèi)型調(diào)用相應(yīng)泛型類(lèi)型中定義的泛型方法,也會(huì)收到警告:

警告顯示原始類(lèi)型繞過(guò)泛型類(lèi)型檢查,將不安全代碼的捕獲推遲到運(yùn)行時(shí)。因此,開(kāi)發(fā)人員應(yīng)該避免使用原始類(lèi)型。

2.6.3 泛型方法

泛型方法是引入其自己的類(lèi)型參數(shù)的方法。這類(lèi)似于聲明泛型類(lèi)型,但類(lèi)型參數(shù)的范圍僅限于聲明它的方法。允許使用靜態(tài)和非靜態(tài)泛型方法以及泛型類(lèi)構(gòu)造函數(shù)。

泛型方法的語(yǔ)法包括一個(gè)類(lèi)型參數(shù)列表,在尖括號(hào)內(nèi),它出現(xiàn)在方法的返回類(lèi)型之前。對(duì)于靜態(tài)泛型方法,類(lèi)型參數(shù)部分必須出現(xiàn)在方法的返回類(lèi)型之前。

在下面的例子中,Util類(lèi)包含一個(gè)泛型方法compare,用于比較兩個(gè)Pair對(duì)象:

compare方法的調(diào)用方式如下:

其中,compare方法的類(lèi)型通??梢允÷?,因?yàn)榫幾g器將推斷所需的類(lèi)型:

2.6.4 有界類(lèi)型參數(shù)

有時(shí)可能希望限制可在參數(shù)化類(lèi)型中用作類(lèi)型參數(shù)的類(lèi)型。例如,對(duì)數(shù)字進(jìn)行操作的方法可能只想接受Number或其子類(lèi)的實(shí)例。這時(shí)就需要用到有界類(lèi)型參數(shù)。

1.聲明有界類(lèi)型參數(shù)

要聲明有界類(lèi)型參數(shù),先要列出類(lèi)型參數(shù)的名稱(chēng),然后是extends關(guān)鍵字,后面跟著它的上限,比如下面例子中的Number:

上面的代碼將會(huì)編譯失敗,報(bào)錯(cuò)如下:

除了限制可用于實(shí)例化泛型類(lèi)型的類(lèi)型之外,有界類(lèi)型參數(shù)還允許調(diào)用邊界中定義的方法:

在上面的例子中,isEven方法通過(guò)n調(diào)用Integer類(lèi)中定義的intValue方法。

2.多個(gè)邊界

前面的示例說(shuō)明了使用帶有單個(gè)邊界的類(lèi)型參數(shù),但是類(lèi)型參數(shù)其實(shí)是可以有多個(gè)邊界的:

具有多個(gè)邊界的類(lèi)型變量是綁定中列出的所有類(lèi)型的子類(lèi)型。如果其中一個(gè)邊界是類(lèi),就必須首先指定它。例如:

如果未首先指定綁定A,就會(huì)出現(xiàn)編譯時(shí)錯(cuò)誤:

注意

在有界類(lèi)型參數(shù)中的extends既可以表示“extends”(類(lèi)中的繼承),也可以表示“implements”(接口中的實(shí)現(xiàn))。

2.6.5 泛型的繼承和子類(lèi)型

在Java中,只要類(lèi)型兼容就可以將一種類(lèi)型的對(duì)象分配給另一種類(lèi)型的對(duì)象。例如,可以將Integer分配給Object,因?yàn)镺bject是Integer的超類(lèi)之一:

在面向?qū)ο蟮男g(shù)語(yǔ)中,這種關(guān)系被稱(chēng)為“is-a”。由于Integer是一種Object,因此允許賦值。但是Integer同時(shí)也是一種Number,所以下面的代碼也是有效的:

在泛型中也是如此??梢詧?zhí)行泛型類(lèi)型調(diào)用,將Number作為其類(lèi)型參數(shù)傳遞。如果參數(shù)與Number兼容,就允許任何后續(xù)的add調(diào)用:

現(xiàn)在考慮下面的方法:

通過(guò)查看其簽名,可以看到上述方法接受一個(gè)類(lèi)型為Box<Number>的參數(shù)。也許你可能會(huì)想當(dāng)然地認(rèn)為這個(gè)方法也能接收Box<Integer>或Box<Double>,答案是否定的,因?yàn)锽ox<Integer>和Box<Double>并不是Box<Number>的子類(lèi)型。在使用泛型編程時(shí),這是一個(gè)常見(jiàn)的誤解,雖然Integer和Double是Number的子類(lèi)型。

圖2-4展示了泛型和子類(lèi)型之間的關(guān)系。

圖2-4 泛型和子類(lèi)型之間的關(guān)系

可以通過(guò)擴(kuò)展或?qū)崿F(xiàn)泛型類(lèi)或接口來(lái)對(duì)其進(jìn)行子類(lèi)型化。一個(gè)類(lèi)或接口的類(lèi)型參數(shù)與另一個(gè)類(lèi)或參數(shù)的類(lèi)型參數(shù)之間的關(guān)系由extends和implements子句確定。

以Collections類(lèi)為例,ArrayList<E>實(shí)現(xiàn)了List<E>,而List<E>擴(kuò)展了Collection<E>,所以ArrayList<String>是List<String>的子類(lèi)型,同時(shí)它也是Collection<String>的子類(lèi)型。只要不改變類(lèi)型參數(shù),就會(huì)在類(lèi)型之間保留子類(lèi)型關(guān)系。圖2-5展示了這些類(lèi)的層次關(guān)系。

圖2-5 泛型類(lèi)及子類(lèi)

現(xiàn)在假設(shè)我們想要定義自己的列表接口PayloadList,它將泛型類(lèi)型P的可選值與每個(gè)元素相關(guān)聯(lián)。它的聲明可能如下:

以下是PayloadList參數(shù)化的List<String>的子類(lèi)型:

· PayloadList<String,String>

· PayloadList<String,Integer>

· PayloadList<String,Exception>

這些類(lèi)的關(guān)系圖如圖2-6所示。

圖2-6 泛型類(lèi)及子類(lèi)之間的關(guān)系

2.6.6 通配符

通配符(?)通常用于表示未知類(lèi)型。通配符可用于各種情況:

· 作為參數(shù)、字段或局部變量的類(lèi)型。

· 作為返回類(lèi)型。

在泛型中,通配符不用于泛型方法調(diào)用,泛型類(lèi)實(shí)例創(chuàng)建或超類(lèi)型的類(lèi)型參數(shù)。

1.上限有界通配符

可以使用上限通配符來(lái)放寬對(duì)變量的限制。例如,要編寫(xiě)一個(gè)適用于List<Integer>、List<Double>和List<Number>的方法,可以通過(guò)使用上限有界通配符來(lái)實(shí)現(xiàn)這一點(diǎn)。比如下面的例子:

可以指定為Integer類(lèi)型:

   List<Integer> li = Arrays.asList(1, 2, 3);
   System.out.println("sum = " + sumOfList(li));

輸出結(jié)果為:

   sum = 6.0

可以指定為Double類(lèi)型:

   List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
   System.out.println("sum = " + sumOfList(ld));

輸出結(jié)果為:

   sum = 7.0
2.無(wú)界通配符

無(wú)界通配符類(lèi)型通常用于定義未知類(lèi)型,比如List<?>。

無(wú)界通配符通常有兩種典型的用法。

第一種是使用Object類(lèi)中提供的功能實(shí)現(xiàn)的方法??紤]以下方法printList:

printList只能打印一個(gè)Object實(shí)例列表,不能打印List<Integer>、List<String>、List<Double>等,因?yàn)樗鼈儾皇荓ist<Object>的子類(lèi)型。

第二種是當(dāng)代碼使用泛型類(lèi)中不依賴(lài)于類(lèi)型參數(shù)的方法。例如List.size或List.clear。實(shí)際上,經(jīng)常使用Class<?>,因?yàn)镃lass<T>中的大多數(shù)方法都不依賴(lài)于T。比如下面的例子:

因?yàn)長(zhǎng)ist<A>是List<?>的子類(lèi),所以可以打印出任何類(lèi)型:

   List<Integer> li = Arrays.asList(1, 2, 3);
   List<String>  ls = Arrays.asList("one", "two", "three");
   printList(li);
   printList(ls);

因此,要區(qū)分場(chǎng)景來(lái)選擇使用List<Object>或是List<?>。如果想插入一個(gè)Object或者是任意Object的子類(lèi),就可以使用List<Object>,但只能在List<?>中插入null。

3.下限有界通配符

下限有界通配符將未知類(lèi)型限制為該類(lèi)型的特定類(lèi)型或超類(lèi)型。使用下限有界通配符的語(yǔ)法為<? super A>。

假設(shè)要編寫(xiě)一個(gè)將Integer對(duì)象放入列表的方法,為了最大限度地提高靈活性,希望該方法可以處理List<Integer>、List<Number>或者是List<Object>等可以保存Integer值的方法。

比如下面的例子將數(shù)字1到10添加到列表的末尾:

4.通配符及其子類(lèi)

可以使用通配符在泛型類(lèi)或接口之間創(chuàng)建關(guān)系。

給定以下兩個(gè)常規(guī)(非泛型)類(lèi):

下面的代碼是成立的:

此示例顯示常規(guī)類(lèi)的繼承遵循此子類(lèi)型規(guī)則:如果B擴(kuò)展A,那么類(lèi)B是類(lèi)A的子類(lèi)型。此規(guī)則不適用于泛型類(lèi)型:

圖2-7 List<Integer>和List<Number>之間的關(guān)系

盡管Integer是Number的子類(lèi)型,但是List<Integer>并不是List<Number>的子類(lèi)型。

為了在這些類(lèi)之間創(chuàng)建關(guān)系,以便代碼可以通過(guò)List<Integer>的元素訪問(wèn)Number的方法,需要使用上限有界通配符:

因?yàn)镮nteger是Number的子類(lèi)型,而numList是Number對(duì)象的列表,所以intList(Integer對(duì)象列表)和numList之間存在關(guān)系。圖2-8顯示了使用上限和下限有界通配符聲明的多個(gè)List類(lèi)之間的關(guān)系。

圖2-8 多個(gè)List類(lèi)之間的關(guān)系

2.6.7 類(lèi)型擦除

泛型被引入到Java語(yǔ)言中,以便在編譯時(shí)提供更嚴(yán)格的類(lèi)型檢查并支持泛型編程。為了實(shí)現(xiàn)泛型,Java編譯器將類(lèi)型擦除應(yīng)用于:

· 如果類(lèi)型參數(shù)是無(wú)界的,則用泛型或?qū)ο筇鎿Q泛型類(lèi)型中的所有類(lèi)型參數(shù)。因此,生成的字節(jié)碼僅包含普通的類(lèi)。

· 如有必要,插入類(lèi)型鑄件以保持類(lèi)型安全。

· 生成橋接方法以保留擴(kuò)展泛型類(lèi)型中的多態(tài)性。

類(lèi)型擦除能夠確保不為參數(shù)化類(lèi)型創(chuàng)建新類(lèi),因此泛型不會(huì)產(chǎn)生運(yùn)行時(shí)開(kāi)銷(xiāo)。

1.擦除泛型類(lèi)型

在類(lèi)型擦除過(guò)程中,Java編譯器將擦除所有的類(lèi)型參數(shù),并在類(lèi)型參數(shù)有界時(shí)將其替換為第一個(gè)綁定,如果類(lèi)型參數(shù)為無(wú)界,就替換為Object。

考慮以下表示單鏈表中節(jié)點(diǎn)的泛型類(lèi):

因?yàn)轭?lèi)型參數(shù)T是無(wú)界的,所以Java編譯器將其替換為Object:

在以下示例中,泛型Node類(lèi)使用有界類(lèi)型參數(shù):

Java編譯器將有界類(lèi)型參數(shù)T替換為第一個(gè)綁定類(lèi)Comparable:

2.擦除泛型方法

Java編譯器還會(huì)擦除泛型方法參數(shù)中的類(lèi)型參數(shù)。請(qǐng)考慮以下泛型方法:

因?yàn)門(mén)是無(wú)界的,所以Java編譯器將會(huì)將它替換為Object:

假設(shè)定義了以下類(lèi):

可以使用泛型方法繪制不同的圖形:

Java編譯器將會(huì)將T替換為Shape:

2.6.8 使用泛型的一些限制

使用泛型,需要考慮以下一些限制。

1.無(wú)法使用基本類(lèi)型實(shí)例化泛型

請(qǐng)考慮以下參數(shù)化類(lèi)型:

創(chuàng)建Pair對(duì)象時(shí),不能將基本類(lèi)型替換為類(lèi)型參數(shù)K或V:

只能將非基本類(lèi)型替換為類(lèi)型參數(shù)K和V:

此時(shí),Java編譯器會(huì)自動(dòng)裝箱,將8轉(zhuǎn)為Integer.valueOf(8),將'a'轉(zhuǎn)為Character('a'):

2.無(wú)法創(chuàng)建類(lèi)型參數(shù)的實(shí)例

無(wú)法創(chuàng)建類(lèi)型參數(shù)的實(shí)例。例如,以下代碼導(dǎo)致編譯時(shí)錯(cuò)誤:

作為解決方法,可以通過(guò)反射創(chuàng)建類(lèi)型參數(shù)的對(duì)象:

可以按如下方式調(diào)用append方法:

3.無(wú)法聲明類(lèi)型為類(lèi)型參數(shù)的靜態(tài)字段

類(lèi)的靜態(tài)字段是類(lèi)的所有非靜態(tài)對(duì)象共享的類(lèi)級(jí)變量。因此,不允許使用類(lèi)型參數(shù)的靜態(tài)字段。考慮以下類(lèi):

若允許類(lèi)型參數(shù)的靜態(tài)字段,則以下代碼將混淆:

靜態(tài)字段os由phone、pager、pc共享,那么os的實(shí)際類(lèi)型是什么呢?它不能同時(shí)是Smartphone、Pager或者TabletPC,因此無(wú)法創(chuàng)建類(lèi)型參數(shù)的靜態(tài)字段。

4.無(wú)法使用具有參數(shù)化類(lèi)型的強(qiáng)制轉(zhuǎn)換或instanceof

因?yàn)镴ava編譯器會(huì)擦除通用代碼中的所有類(lèi)型參數(shù),所以無(wú)法驗(yàn)證在運(yùn)行時(shí)使用泛型類(lèi)型的參數(shù)化類(lèi)型:

傳遞給rtti方法的參數(shù)化類(lèi)型集是:

   S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>}

運(yùn)行時(shí)不跟蹤類(lèi)型參數(shù),因此無(wú)法區(qū)分ArrayList<Integer>和ArrayList<String>,最多是使用無(wú)界通配符來(lái)驗(yàn)證列表是否為ArrayList:

通常,除非通過(guò)無(wú)界通配符進(jìn)行參數(shù)化,否則無(wú)法強(qiáng)制轉(zhuǎn)換為參數(shù)化類(lèi)型。例如:

在某些情況下,編譯器知道類(lèi)型參數(shù)始終有效并允許強(qiáng)制轉(zhuǎn)換。例如:

   List<String> l1 = ...;
   ArrayList<String> l2 = (ArrayList<String>)l1;  // 正確
5.無(wú)法創(chuàng)建參數(shù)化類(lèi)型的數(shù)組

無(wú)法創(chuàng)建參數(shù)化類(lèi)型的數(shù)組。例如,以下代碼無(wú)法編譯:

以下代碼說(shuō)明將不同類(lèi)型插入到數(shù)組中時(shí)會(huì)發(fā)生什么:

如果使用通用列表嘗試相同的操作,就會(huì)出現(xiàn)問(wèn)題:

如果允許參數(shù)化列表數(shù)組,那么前面的代碼將無(wú)法拋出所需的ArrayStoreException。

6.無(wú)法創(chuàng)建、捕獲或拋出參數(shù)化類(lèi)型的對(duì)象

泛型類(lèi)不能直接或間接擴(kuò)展Throwable類(lèi)。例如,以下類(lèi)將無(wú)法編譯:

方法無(wú)法捕獲類(lèi)型參數(shù)的實(shí)例:

但是可以在throws子句中使用類(lèi)型參數(shù):

7.類(lèi)型擦除到原生類(lèi)型的方法無(wú)法重載

類(lèi)不能有兩個(gè)重載方法,因?yàn)樗鼈冊(cè)陬?lèi)型擦除后具有相同的簽名。觀察下面的例子:

上述例子將產(chǎn)生編譯時(shí)錯(cuò)誤。

主站蜘蛛池模板: 泸西县| 武乡县| 茌平县| 灵宝市| 遂昌县| 台中市| 诏安县| 徐闻县| 兴仁县| 类乌齐县| 亳州市| 灵川县| 河池市| 安陆市| 镇安县| 城固县| 东乡族自治县| 绩溪县| 吉林省| 巴林左旗| 方山县| 永年县| 察隅县| 长泰县| 定边县| 星子县| 五家渠市| 祁东县| 绵竹市| 厦门市| 廊坊市| 类乌齐县| 广宁县| 和静县| 南昌市| 九龙坡区| 德阳市| 读书| 襄樊市| 正安县| 丹东市|