Java5.0的新特性之一是引入了泛型類型和泛型方法。一個(gè)泛型類型通過(guò)使用一個(gè)或多個(gè)類型變量來(lái)定義,并擁有一個(gè)或多個(gè)使用一個(gè)類型變量作為一個(gè)參數(shù)或者返回值的占位符。例如,類型java.util.List<E>是一個(gè)泛型類型:一個(gè)list,其元素的類型被占位符E描述。這個(gè)類型有一個(gè)名為add()的方法,被聲明為有一個(gè)類型為E的參數(shù),同時(shí),有一個(gè)get()方法,返回值被聲明為E類型。
為了使用泛型類型,你應(yīng)該為類型變量詳細(xì)指明實(shí)際的類型,形成一個(gè)就像List<String>類似的參數(shù)化類型。[1]指明這些額外的類型信息的原因是編譯器據(jù)此能夠在編譯期為您提供很強(qiáng)的類型檢查,增強(qiáng)您的程序的類型安全性。舉個(gè)例子來(lái)說(shuō),您有一個(gè)只能保持String對(duì)象的List,那么這種類型檢查就能夠阻止您往里面加入String[]對(duì)象。同樣的,增加的類型信息使編譯器能夠?yàn)槟鲆恍╊愋娃D(zhuǎn)換的事情。比如,編譯器知道了一個(gè)List<String>有個(gè)get()方法,其返回值是一個(gè)String對(duì)象,因此您不再需要去將返回值由一個(gè)Object強(qiáng)制轉(zhuǎn)換為String。
Java.util包中的集合類在java5.0中已經(jīng)被做成了泛型,也許您將會(huì)在您的程序中頻繁的使用到他們。類型安全的集合類就是一個(gè)泛型類型的典型案例。即便您從沒(méi)有定義過(guò)您自己的泛型類型甚至從未用過(guò)除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個(gè)標(biāo)志——他們證明了這個(gè)主要的新語(yǔ)言特性的復(fù)雜性。
我們從探索類型安全的集合類中的基本的泛型用法開(kāi)始,進(jìn)而研究更多使用泛型類型的復(fù)雜細(xì)節(jié)。然后我們討論類型參數(shù)通配符和有界通配符。描繪了如何使用泛型以后,我們闡明如何編寫(xiě)自己的泛型類型和泛型方法。我們對(duì)于泛型的討論將結(jié)束于一趟對(duì)于JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對(duì)泛型如何工作這個(gè)問(wèn)題有個(gè)深入的理解。
類型安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對(duì)象的set、對(duì)象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數(shù)來(lái)界定集合中的對(duì)象的類型。這個(gè)討論并不適合java1.4或更早期版本。如果沒(méi)有泛型,對(duì)于集合類的使用需要程序員記住每個(gè)集合中元素的類型。當(dāng)您在java1.4種創(chuàng)建了一個(gè)集合,您知道您放入到集合中的對(duì)象的類型,但是編譯器不知道。您必須小心地往其中加入一個(gè)合適類型的元素,當(dāng)需要從集合中獲取元素時(shí),您必須顯式的寫(xiě)強(qiáng)制類型轉(zhuǎn)換以將他們從Object轉(zhuǎn)換為他們真是的類型??疾煜逻叺膉ava1.4的代碼。
在java5.0中,當(dāng)我們申明一個(gè)List或者創(chuàng)建一個(gè)ArrayList的實(shí)例的時(shí)候,我們需要在泛型類型的名字后面緊跟一對(duì)“<>”,尖括號(hào)中寫(xiě)入我們需要的實(shí)際的類型。比如,一個(gè)保持String的List應(yīng)該寫(xiě)成“List<String>”。需要注意的是,這非常象給一個(gè)方法傳一個(gè)參數(shù),區(qū)別是我們使用類型而不是值,同時(shí)使用尖括號(hào)而不是圓括號(hào)
Java.util的集合類中的元素必須是對(duì)象化的,他們不能是基本類型。泛型的引入并沒(méi)有改變這點(diǎn)。泛型不能使用基本類型:我們不能這樣來(lái)申明——Set<char>或者List<int>。記住,無(wú)論如何,java5.0中的自動(dòng)打包和自動(dòng)解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。(查看第二章以了解更多關(guān)于自動(dòng)打包和自動(dòng)解包的細(xì)節(jié))。
在Java5.0中,上面的例子將被重寫(xiě)為如下方式:
就像一個(gè)方法可以使用任意數(shù)量的參數(shù)一樣,類允許使用多個(gè)類型變量。接口Java.util.Map就是一個(gè)例子。一個(gè)Map體現(xiàn)了從一個(gè)key的對(duì)象到一個(gè)value的對(duì)象的映射關(guān)系。接口Map申明了一個(gè)類型變量來(lái)描述key的類型而另一個(gè)類型變量來(lái)描述value的類型。舉個(gè)例子來(lái)說(shuō),假設(shè)您希望做一個(gè)String對(duì)象到Integer對(duì)象的映射關(guān)系:
理解泛型類型
本段將對(duì)泛型類型的使用細(xì)節(jié)做進(jìn)一步的探討,以嘗試說(shuō)明下列問(wèn)題:
不帶類型參數(shù)的使用泛型的后果
參數(shù)化類型的體系
一個(gè)關(guān)于編譯期泛型類型的類型安全的漏洞和一個(gè)用于確保運(yùn)行期類型安全的補(bǔ)丁
為什么參數(shù)化類型的數(shù)組不是類型安全的
未經(jīng)處理的類型和不被檢查的警告
即使被重寫(xiě)的Java集合類帶來(lái)了泛型的好處,在使用他們的時(shí)候您也不被要求說(shuō)明類型變量。一個(gè)不帶類型變量的泛型類型被認(rèn)為是一個(gè)未經(jīng)處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運(yùn)行:您顯式的編寫(xiě)所有類型轉(zhuǎn)換就像您已經(jīng)這樣寫(xiě)的一樣,您可能會(huì)被一些來(lái)自編譯器的麻煩所困擾。查看下列存儲(chǔ)不同類型的對(duì)象到一個(gè)未經(jīng)處理的List:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint參數(shù)后重新編譯,我們會(huì)看到這些警告:
Test.java:6: warning: [unchecked]
????unchecked call to add(E) as a member of the raw type java.util.List
????????l.add("hello");??
???????? ^
Test.java:7: warning: [unchecked]
????unchecked call to add(E) as a member of the raw type java.util.List
????????l.add(new Integer(123));
???????? ^
編譯在add()方法的調(diào)用上給出了警告,因?yàn)樗荒軌虼_信加入到list中的值具有正確的類型。它告訴我們說(shuō)我們使用了一個(gè)未經(jīng)處理的類型,它不能驗(yàn)證我們的代碼是類型安全的。注意,get()方法的調(diào)用是沒(méi)有問(wèn)題的,因?yàn)槟軌虮猾@得的元素已經(jīng)安全的存在于list中了。
如果您不想使用任何的java5.0的新特性,您可以簡(jiǎn)單的通過(guò)帶-source1.4標(biāo)記來(lái)編譯他們,這樣編譯器就不會(huì)再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過(guò)使用一個(gè)“@SuppressWarnings("unchecked")”注解(查看本章的4.3節(jié))隱瞞這些警告信息或者升級(jí)您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時(shí)候不再會(huì)有警告但仍然允許您往list中放入不同的類型的對(duì)象。
參數(shù)化類型有類型體系,就像一般的類型一樣。這個(gè)體系基于對(duì)象的類型,而不是變量的類型。這里有些例子您可以嘗試:
運(yùn)行時(shí)類型安全
就像我們所見(jiàn)到的,一個(gè)List<X>不允許被轉(zhuǎn)換為一個(gè)List<Y>,即使這個(gè)X能夠被轉(zhuǎn)換為Y。然而,一個(gè)List<X>能夠被轉(zhuǎn)換為一個(gè)List,這樣您就可以通過(guò)繼承的方法來(lái)做這樣的事情。
這種將參數(shù)化類型轉(zhuǎn)換為非參數(shù)化類型的能力對(duì)于向下兼容是必要的,但是它會(huì)在泛型所帶來(lái)的類型安全體系上鑿個(gè)漏洞:
在使用泛型類型的時(shí)候,數(shù)組需要特別的考慮?;貞浺幌?,如果T是S的父類(或者接口),那么類型為S的數(shù)組S[],同時(shí)又是類型為T(mén)的數(shù)組T[]。正因?yàn)槿绱?,每次您存放一個(gè)對(duì)象到數(shù)組中時(shí),Java解釋器都必須進(jìn)行檢查以確保您放入的對(duì)象類型與要存放的數(shù)組所允許的類型是匹對(duì)的。例如,下列代碼在運(yùn)行期會(huì)檢查失敗,拋出一個(gè)ArrayStoreException異常:
當(dāng)我們使用泛型類型的時(shí)候,僅僅依靠運(yùn)行時(shí)的數(shù)組存放異常檢查是不夠的,因?yàn)橐粋€(gè)運(yùn)行時(shí)進(jìn)行的檢查并不能夠獲取編譯時(shí)的類型參數(shù)信息。查看下列代碼:
注意這并不是一個(gè)在使用數(shù)組時(shí)使用泛型的全部的約束,這僅僅是一個(gè)創(chuàng)建一個(gè)參數(shù)化類型數(shù)組的約束。我們將在學(xué)習(xí)如何寫(xiě)泛型方法時(shí)再來(lái)討論這個(gè)話題。
類型參數(shù)通配符
假設(shè)我們需要寫(xiě)一個(gè)方法來(lái)顯示一個(gè)List中的元素。[3]在以前,我們只需要象這樣寫(xiě)段代碼:
作為一般原則,如果類型是泛型的,同時(shí)您并不知道或者并不關(guān)心值的類型,您應(yīng)該使用“?”通配符來(lái)代替一個(gè)未經(jīng)處理的類型。未經(jīng)處理的類型被允許僅是為了向下兼容,而且應(yīng)該只能夠被允許出現(xiàn)在老的代碼中。注意,無(wú)論如何,您不能在調(diào)用構(gòu)造器時(shí)使用通配符。下面的代碼是非法的:
List<?> l = new ArrayList<?>();
創(chuàng)建一個(gè)不知道類型的List是毫無(wú)道理的。如果您創(chuàng)建了它,那么您必須知道它將保持的元素是什么類型的。您可以在隨后的方法中不關(guān)心元素類型而去遍歷這里list,但是您需要在您創(chuàng)建它的時(shí)候描述元素的類型。如果你確實(shí)需要一個(gè)List來(lái)保持任何類型,那么您只能這么寫(xiě):
第二,考察List的類似add()的方法,他們被聲明為接受一個(gè)參數(shù),這個(gè)參數(shù)被類型參數(shù)所定義。出人意料的是,當(dāng)類型參數(shù)是未確定的,編譯器不允許您調(diào)用任何有不確定參數(shù)類型的方法——因?yàn)樗荒艽_認(rèn)您傳入了一個(gè)恰當(dāng)?shù)闹?。一個(gè)List(?)實(shí)際上是只讀的——既然編譯器不允許我們調(diào)用類似于add(),set(),addAll()這類的方法。
界定通配符
讓我們?cè)谖覀冊(cè)瓉?lái)的例子上作些小小的稍微復(fù)雜一點(diǎn)的改動(dòng)。假設(shè)我們希望寫(xiě)一個(gè)sumList()方法來(lái)計(jì)算list中Number類型的值的合計(jì)。在以前,我們使用未經(jīng)處理的List,但是我們不想放棄類型安全,同時(shí)不得不處理來(lái)自編譯器的unchecked警告?;蛘呶覀兛梢允褂肔ist<Number>,那樣的話我們就不能調(diào)用List<Integer>、List<Double>中的方法了,而事實(shí)上我們需要調(diào)用。如果我們使用通配符,那么我們實(shí)際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調(diào)用,Number?還是Number的子類?甚至,String?這樣的一個(gè)方法也許會(huì)被寫(xiě)成這樣:
注意,這樣的話,那些類型轉(zhuǎn)換已經(jīng)不再需要了。我們并不知道list中元素的具體類型,但是我們知道他們能夠向上轉(zhuǎn)型為Number,因此我們可以把他們從list中把他們當(dāng)作一個(gè)Number對(duì)象取出。使用一個(gè)for/in循環(huán)能夠稍微封裝一下從list中取出元素的過(guò)程。普遍性的原則是當(dāng)您使用一個(gè)界定通配符時(shí),類似于List中的get()方法的那些方法將返回一個(gè)類型為上界的值。因此如果我們?cè)趂or/in循環(huán)中調(diào)用list.get(),我們將得到一個(gè)Number。在前一節(jié)說(shuō)到使用通配符時(shí)類似于list.add()這種方法中的限制依然有效:舉個(gè)例子來(lái)說(shuō),如果編譯器允許我們調(diào)用這類方法,我們就可以將一個(gè)Integer放到一個(gè)聲明為僅保持Short值的list中去。
同樣可行的是使用下界通配符,不同的是用super替換extends。這個(gè)技巧在被調(diào)用的方法上有一點(diǎn)不同的作用。在實(shí)際應(yīng)用中,下界通配符要比上界通配符用得少。我們將在后面的章節(jié)里討論這個(gè)問(wèn)題。
為了使用泛型類型,你應(yīng)該為類型變量詳細(xì)指明實(shí)際的類型,形成一個(gè)就像List<String>類似的參數(shù)化類型。[1]指明這些額外的類型信息的原因是編譯器據(jù)此能夠在編譯期為您提供很強(qiáng)的類型檢查,增強(qiáng)您的程序的類型安全性。舉個(gè)例子來(lái)說(shuō),您有一個(gè)只能保持String對(duì)象的List,那么這種類型檢查就能夠阻止您往里面加入String[]對(duì)象。同樣的,增加的類型信息使編譯器能夠?yàn)槟鲆恍╊愋娃D(zhuǎn)換的事情。比如,編譯器知道了一個(gè)List<String>有個(gè)get()方法,其返回值是一個(gè)String對(duì)象,因此您不再需要去將返回值由一個(gè)Object強(qiáng)制轉(zhuǎn)換為String。
Java.util包中的集合類在java5.0中已經(jīng)被做成了泛型,也許您將會(huì)在您的程序中頻繁的使用到他們。類型安全的集合類就是一個(gè)泛型類型的典型案例。即便您從沒(méi)有定義過(guò)您自己的泛型類型甚至從未用過(guò)除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個(gè)標(biāo)志——他們證明了這個(gè)主要的新語(yǔ)言特性的復(fù)雜性。
我們從探索類型安全的集合類中的基本的泛型用法開(kāi)始,進(jìn)而研究更多使用泛型類型的復(fù)雜細(xì)節(jié)。然后我們討論類型參數(shù)通配符和有界通配符。描繪了如何使用泛型以后,我們闡明如何編寫(xiě)自己的泛型類型和泛型方法。我們對(duì)于泛型的討論將結(jié)束于一趟對(duì)于JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對(duì)泛型如何工作這個(gè)問(wèn)題有個(gè)深入的理解。
類型安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對(duì)象的set、對(duì)象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數(shù)來(lái)界定集合中的對(duì)象的類型。這個(gè)討論并不適合java1.4或更早期版本。如果沒(méi)有泛型,對(duì)于集合類的使用需要程序員記住每個(gè)集合中元素的類型。當(dāng)您在java1.4種創(chuàng)建了一個(gè)集合,您知道您放入到集合中的對(duì)象的類型,但是編譯器不知道。您必須小心地往其中加入一個(gè)合適類型的元素,當(dāng)需要從集合中獲取元素時(shí),您必須顯式的寫(xiě)強(qiáng)制類型轉(zhuǎn)換以將他們從Object轉(zhuǎn)換為他們真是的類型??疾煜逻叺膉ava1.4的代碼。
public static void main(String[] args) {泛型類型解決了這段代碼中的顯示的類型安全問(wèn)題。Java.util中的List或是其他集合類已經(jīng)使用泛型重寫(xiě)過(guò)了。就像前面提到的, List被重新定義為一個(gè)list,它中間的元素類型被一個(gè)類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個(gè)類型為E的參數(shù),用于替換以前的Object,get()方法被重新定義為返回一個(gè)E,替換了以前的Object。
????// This list is intended to hold only strings.
????// The compiler doesn't know that so we have to remember ourselves.
????List wordlist = new ArrayList();??
????// Oops! We added a String[] instead of a String.
????// The compiler doesn't know that this is an error.
????wordlist.add(args);
????// Since List can hold arbitrary objects, the get() method returns
????// Object.??Since the list is intended to hold strings, we cast the
????// return value to String but get a ClassCastException because of
????// the error above.
????String word = (String)wordlist.get(0);
}
在java5.0中,當(dāng)我們申明一個(gè)List或者創(chuàng)建一個(gè)ArrayList的實(shí)例的時(shí)候,我們需要在泛型類型的名字后面緊跟一對(duì)“<>”,尖括號(hào)中寫(xiě)入我們需要的實(shí)際的類型。比如,一個(gè)保持String的List應(yīng)該寫(xiě)成“List<String>”。需要注意的是,這非常象給一個(gè)方法傳一個(gè)參數(shù),區(qū)別是我們使用類型而不是值,同時(shí)使用尖括號(hào)而不是圓括號(hào)
Java.util的集合類中的元素必須是對(duì)象化的,他們不能是基本類型。泛型的引入并沒(méi)有改變這點(diǎn)。泛型不能使用基本類型:我們不能這樣來(lái)申明——Set<char>或者List<int>。記住,無(wú)論如何,java5.0中的自動(dòng)打包和自動(dòng)解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。(查看第二章以了解更多關(guān)于自動(dòng)打包和自動(dòng)解包的細(xì)節(jié))。
在Java5.0中,上面的例子將被重寫(xiě)為如下方式:
public static void main(String[] args) {值得注意的是代碼量其實(shí)并沒(méi)有比原來(lái)那個(gè)沒(méi)有泛型的例子少多少。使用“(String)”這樣的類型轉(zhuǎn)換被替換成了類型參數(shù)“<String>”。 不同的是類型參數(shù)需要且僅需要聲明一次,而list能夠被使用任何多次,不需要類型轉(zhuǎn)換。在更長(zhǎng)點(diǎn)的例子代碼中,這一點(diǎn)將更加明顯。即使在那些看上去泛型語(yǔ)法比非泛型語(yǔ)法要冗長(zhǎng)的例子里,使用泛型依然是非常有價(jià)值的——額外的類型信息允許編譯器在您的代碼里執(zhí)行更強(qiáng)的錯(cuò)誤檢查。以前只能在運(yùn)行起才能發(fā)現(xiàn)的錯(cuò)誤現(xiàn)在能夠在編譯時(shí)就被發(fā)現(xiàn)。此外,以前為了處理類型轉(zhuǎn)換的異常,我們需要添加額外的代碼行。如果沒(méi)有泛型,那么當(dāng)發(fā)生類型轉(zhuǎn)換異常的時(shí)候,一個(gè)ClassCastException異常就會(huì)被從實(shí)際代碼中拋出。
????// This list can only hold String objects
????List<String> wordlist = new ArrayList<String>();
????// args is a String[], not String, so the compiler won't let us do this
????wordlist.add(args);??// Compilation error!
????// We can do this, though.??
????// Notice the use of the new for/in looping statement
????for(String arg : args) wordlist.add(arg);
????// No cast is required.??List<String>.get() returns a String.
????String word = wordlist.get(0);
}
就像一個(gè)方法可以使用任意數(shù)量的參數(shù)一樣,類允許使用多個(gè)類型變量。接口Java.util.Map就是一個(gè)例子。一個(gè)Map體現(xiàn)了從一個(gè)key的對(duì)象到一個(gè)value的對(duì)象的映射關(guān)系。接口Map申明了一個(gè)類型變量來(lái)描述key的類型而另一個(gè)類型變量來(lái)描述value的類型。舉個(gè)例子來(lái)說(shuō),假設(shè)您希望做一個(gè)String對(duì)象到Integer對(duì)象的映射關(guān)系:
public static void main(String[] args) {象List<String>這個(gè)一個(gè)參數(shù)類型其本身也是也一個(gè)類型,也能夠被用于當(dāng)作其他類型的一個(gè)類型變量值。您可能會(huì)看到這樣的代碼:
????// A map from strings to their position in the args[] array
????Map<String,Integer> map = new HashMap<String,Integer>();
????// Note that we use autoboxing to wrap i in an Integer object.
????for(int i=0; i < args.length; i++) map.put(args[i], i);??
????// Find the array index of a word.??Note no cast is required!
????Integer position = map.get("hello");
????// We can also rely on autounboxing to convert directly to an int,
????// but this throws a NullPointerException if the key does not exist
????// in the map
????int pos = map.get("world");
}
// Look at all those nested angle brackets!在上面的代碼里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個(gè)類型為E的list元素或者一個(gè)類型為V的map元素。注意,無(wú)論如何,泛型類型能夠更精密的使用他們的變量。在本書(shū)中的參考章節(jié)查看List<E>,您將會(huì)看到它的iterator( )方法被聲明為返回一個(gè)Iterator<E>。這意味著,這個(gè)方法返回一個(gè)跟list的實(shí)際的參數(shù)類型一樣的一個(gè)參數(shù)類型的實(shí)例。為了具體的說(shuō)明這點(diǎn),下面的例子提供了不使用get(0)方法來(lái)獲取一個(gè)List<String>的第一個(gè)元素的方法
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting.??We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
List<String> words = // ...initialized elsewhere...。
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
理解泛型類型
本段將對(duì)泛型類型的使用細(xì)節(jié)做進(jìn)一步的探討,以嘗試說(shuō)明下列問(wèn)題:
不帶類型參數(shù)的使用泛型的后果
參數(shù)化類型的體系
一個(gè)關(guān)于編譯期泛型類型的類型安全的漏洞和一個(gè)用于確保運(yùn)行期類型安全的補(bǔ)丁
為什么參數(shù)化類型的數(shù)組不是類型安全的
未經(jīng)處理的類型和不被檢查的警告
即使被重寫(xiě)的Java集合類帶來(lái)了泛型的好處,在使用他們的時(shí)候您也不被要求說(shuō)明類型變量。一個(gè)不帶類型變量的泛型類型被認(rèn)為是一個(gè)未經(jīng)處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運(yùn)行:您顯式的編寫(xiě)所有類型轉(zhuǎn)換就像您已經(jīng)這樣寫(xiě)的一樣,您可能會(huì)被一些來(lái)自編譯器的麻煩所困擾。查看下列存儲(chǔ)不同類型的對(duì)象到一個(gè)未經(jīng)處理的List:
List l = new ArrayList();這段代碼在java1.4下運(yùn)行得很好。如果您用java5.0來(lái)編譯它,javac編譯了,但是會(huì)打印出這樣的“抱怨”:
l.add("hello");??
l.add(new Integer(123));
Object o = l.get(0);
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint參數(shù)后重新編譯,我們會(huì)看到這些警告:
Test.java:6: warning: [unchecked]
????unchecked call to add(E) as a member of the raw type java.util.List
????????l.add("hello");??
???????? ^
Test.java:7: warning: [unchecked]
????unchecked call to add(E) as a member of the raw type java.util.List
????????l.add(new Integer(123));
???????? ^
編譯在add()方法的調(diào)用上給出了警告,因?yàn)樗荒軌虼_信加入到list中的值具有正確的類型。它告訴我們說(shuō)我們使用了一個(gè)未經(jīng)處理的類型,它不能驗(yàn)證我們的代碼是類型安全的。注意,get()方法的調(diào)用是沒(méi)有問(wèn)題的,因?yàn)槟軌虮猾@得的元素已經(jīng)安全的存在于list中了。
如果您不想使用任何的java5.0的新特性,您可以簡(jiǎn)單的通過(guò)帶-source1.4標(biāo)記來(lái)編譯他們,這樣編譯器就不會(huì)再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過(guò)使用一個(gè)“@SuppressWarnings("unchecked")”注解(查看本章的4.3節(jié))隱瞞這些警告信息或者升級(jí)您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時(shí)候不再會(huì)有警告但仍然允許您往list中放入不同的類型的對(duì)象。
List<Object> l = new ArrayList<Object>();參數(shù)化類型的體系
l.add("hello");??
l.add(123);??????????????// autoboxing
Object o = l.get(0);
參數(shù)化類型有類型體系,就像一般的類型一樣。這個(gè)體系基于對(duì)象的類型,而不是變量的類型。這里有些例子您可以嘗試:
ArrayList<Integer> l = new ArrayList<Integer>();一個(gè)List<Integer>是一個(gè)Collection<Integer>,但不是一個(gè)List<Object>。這句話不容易理解,如果您想理解為什么泛型這樣做,這段值得看一下。考察這段代碼:
List<Integer> m = l;????????????????????????????// okay
Collection<Integer> n = l;??????????????????????// okay
ArrayList<Number> o = l;????????????????????????// error
Collection<Object> p = (Collection<Object>)l;?? // error, even with cast
List<Integer> li = new ArrayList<Integer>();這就是為什么List<Integer>不是一個(gè)List<Object>的原因,雖然List<Integer>中所有的元素事實(shí)上是一個(gè)Object的實(shí)例。如果允許轉(zhuǎn)換成List<Object>,那么轉(zhuǎn)換后,理論上非整型的對(duì)象也將被允許添加到list中。
li.add(123);
// The line below will not compile.??But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;??
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1);??// Can't cast a String to Integer!
運(yùn)行時(shí)類型安全
就像我們所見(jiàn)到的,一個(gè)List<X>不允許被轉(zhuǎn)換為一個(gè)List<Y>,即使這個(gè)X能夠被轉(zhuǎn)換為Y。然而,一個(gè)List<X>能夠被轉(zhuǎn)換為一個(gè)List,這樣您就可以通過(guò)繼承的方法來(lái)做這樣的事情。
這種將參數(shù)化類型轉(zhuǎn)換為非參數(shù)化類型的能力對(duì)于向下兼容是必要的,但是它會(huì)在泛型所帶來(lái)的類型安全體系上鑿個(gè)漏洞:
// Here's a basic parameterized list.泛型僅提供了編譯期的類型安全。如果您使用java5.0的編譯器來(lái)編譯您的代碼并且沒(méi)有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運(yùn)行期也是類型安全的。如果您獲得了警告或者使用了像未經(jīng)處理的類型那樣修改您的集合的代碼,那么您需要增加一些步驟來(lái)確保運(yùn)行期的類型安全。您可以通過(guò)使用java.util.Collections中的checkedList()和checkedMap( )方法來(lái)做到這一步。這些方法將把您的集合打包成一個(gè)wrapper集合,從而在運(yùn)行時(shí)檢查確認(rèn)只有正確類型的值能夠被置入集合眾。下面是一個(gè)能夠補(bǔ)上類型安全漏洞的一個(gè)例子:
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;??
// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.??
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
// Here's a basic parameterized list.參數(shù)化類型的數(shù)組
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;??
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
在使用泛型類型的時(shí)候,數(shù)組需要特別的考慮?;貞浺幌?,如果T是S的父類(或者接口),那么類型為S的數(shù)組S[],同時(shí)又是類型為T(mén)的數(shù)組T[]。正因?yàn)槿绱?,每次您存放一個(gè)對(duì)象到數(shù)組中時(shí),Java解釋器都必須進(jìn)行檢查以確保您放入的對(duì)象類型與要存放的數(shù)組所允許的類型是匹對(duì)的。例如,下列代碼在運(yùn)行期會(huì)檢查失敗,拋出一個(gè)ArrayStoreException異常:
String[] words = new String[10];雖然編譯時(shí)obj是一個(gè)Object[],但是在運(yùn)行時(shí)它是一個(gè)String[],它不允許被用于存放一個(gè)Integer。
Object[] objs = words;
objs[0] = 1;??// 1 autoboxed to an Integer, throws ArrayStoreException
當(dāng)我們使用泛型類型的時(shí)候,僅僅依靠運(yùn)行時(shí)的數(shù)組存放異常檢查是不夠的,因?yàn)橐粋€(gè)運(yùn)行時(shí)進(jìn)行的檢查并不能夠獲取編譯時(shí)的類型參數(shù)信息。查看下列代碼:
List<String>[] wordlists = new ArrayList<String>[10];如果上面的代碼被允許,那么運(yùn)行時(shí)的數(shù)組存儲(chǔ)檢查將會(huì)成功:沒(méi)有編譯時(shí)的類型參數(shù),代碼簡(jiǎn)單地存儲(chǔ)一個(gè)ArrayList到一個(gè)ArrayList[]數(shù)組,非常正確。既然編譯器不能阻止您通過(guò)這個(gè)方法來(lái)戰(zhàn)勝類型安全,那么它轉(zhuǎn)而阻止您創(chuàng)建一個(gè)參數(shù)化類型的數(shù)組。所以上述情節(jié)永遠(yuǎn)不會(huì)發(fā)生,編譯器在第一行就開(kāi)始拒絕編譯了。
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali;?????????????????????? // No ArrayStoreException
String s = wordlists[0].get(0);??????// ClassCastException!
注意這并不是一個(gè)在使用數(shù)組時(shí)使用泛型的全部的約束,這僅僅是一個(gè)創(chuàng)建一個(gè)參數(shù)化類型數(shù)組的約束。我們將在學(xué)習(xí)如何寫(xiě)泛型方法時(shí)再來(lái)討論這個(gè)話題。
類型參數(shù)通配符
假設(shè)我們需要寫(xiě)一個(gè)方法來(lái)顯示一個(gè)List中的元素。[3]在以前,我們只需要象這樣寫(xiě)段代碼:
public static void printList(PrintWriter out, List list) {
????for(int i=0, n=list.size(); i < n; i++) {
????????if (i > 0) out.print(", ");
????????out.print(list.get(i).toString());
????}
}
在Java5.0中,List是一個(gè)泛型類型,如果我們?cè)噲D編譯這個(gè)方法,我們將會(huì)得到unchecked警告。為了解決這些警告,您可能需要這樣來(lái)修改這個(gè)方法:
public static void printList(PrintWriter out, List<Object> list) {這段代碼能夠編譯通過(guò)同時(shí)不會(huì)有警告,但是它并不是非常地有效,因?yàn)橹挥心切┍宦暶鳛長(zhǎng)ist<Object>的list才會(huì)被允許使用這個(gè)方法。還記得么,類似于List<String>和List<Integer>這樣的List并不能被轉(zhuǎn)型為L(zhǎng)ist<Object>。事實(shí)上我們需要一個(gè)類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關(guān)心它被參數(shù)化為什么。解決辦法是使用類型參數(shù)通配符。方法可以被修改成這樣:
????for(int i=0, n=list.size(); i < n; i++) {
????????if (i > 0) out.print(", ");
????????out.print(list.get(i).toString());
????}
}
public static void printList(PrintWriter out, List<?> list) {這個(gè)版本的方法能夠被編譯過(guò),沒(méi)有警告,而且能夠在任何我們希望使用的地方使用。通配符“?”表示一個(gè)未知類型,類型List<?>被讀作“List of unknown”
????for(int i=0, n=list.size(); i < n; i++) {
????????if (i > 0) out.print(", ");
????????Object o = list.get(i);
????????out.print(o.toString());
????}
}
作為一般原則,如果類型是泛型的,同時(shí)您并不知道或者并不關(guān)心值的類型,您應(yīng)該使用“?”通配符來(lái)代替一個(gè)未經(jīng)處理的類型。未經(jīng)處理的類型被允許僅是為了向下兼容,而且應(yīng)該只能夠被允許出現(xiàn)在老的代碼中。注意,無(wú)論如何,您不能在調(diào)用構(gòu)造器時(shí)使用通配符。下面的代碼是非法的:
List<?> l = new ArrayList<?>();
創(chuàng)建一個(gè)不知道類型的List是毫無(wú)道理的。如果您創(chuàng)建了它,那么您必須知道它將保持的元素是什么類型的。您可以在隨后的方法中不關(guān)心元素類型而去遍歷這里list,但是您需要在您創(chuàng)建它的時(shí)候描述元素的類型。如果你確實(shí)需要一個(gè)List來(lái)保持任何類型,那么您只能這么寫(xiě):
List<Object> l = new ArrayList<Object>();從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個(gè)未經(jīng)處理的List。一個(gè)使用通配符的List<?>有兩個(gè)重要的特性。第一,考察類似于get()的方法,他們被聲明返回一個(gè)值,這個(gè)值的類型是類型參數(shù)中指定的。在這個(gè)例子中,類型是“unknown”,所以這些方法返回一個(gè)Object。既然我們期望的是調(diào)用這個(gè)object的toString()方法,程序能夠很好的滿足我們的意愿。
第二,考察List的類似add()的方法,他們被聲明為接受一個(gè)參數(shù),這個(gè)參數(shù)被類型參數(shù)所定義。出人意料的是,當(dāng)類型參數(shù)是未確定的,編譯器不允許您調(diào)用任何有不確定參數(shù)類型的方法——因?yàn)樗荒艽_認(rèn)您傳入了一個(gè)恰當(dāng)?shù)闹?。一個(gè)List(?)實(shí)際上是只讀的——既然編譯器不允許我們調(diào)用類似于add(),set(),addAll()這類的方法。
界定通配符
讓我們?cè)谖覀冊(cè)瓉?lái)的例子上作些小小的稍微復(fù)雜一點(diǎn)的改動(dòng)。假設(shè)我們希望寫(xiě)一個(gè)sumList()方法來(lái)計(jì)算list中Number類型的值的合計(jì)。在以前,我們使用未經(jīng)處理的List,但是我們不想放棄類型安全,同時(shí)不得不處理來(lái)自編譯器的unchecked警告?;蛘呶覀兛梢允褂肔ist<Number>,那樣的話我們就不能調(diào)用List<Integer>、List<Double>中的方法了,而事實(shí)上我們需要調(diào)用。如果我們使用通配符,那么我們實(shí)際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調(diào)用,Number?還是Number的子類?甚至,String?這樣的一個(gè)方法也許會(huì)被寫(xiě)成這樣:
public static double sumList(List<?> list) {要修改這個(gè)方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數(shù)是未知的,但又是Number或者Number的子類。下面的代碼才是我們想要的:
????double total = 0.0;
????for(Object o : list) {
????????Number n = (Number) o;??// A cast is required and may fail
????????total += n.doubleValue();
????}
????return total;
}
public static double sumList(List<? extends Number> list) {類型List<? extends Number>可以被理解為“Number未知子類的List”。理解這點(diǎn)非常重要,在這段文字中,Number被認(rèn)為是其自身的子類。
????double total = 0.0;
????for(Number n : list) total += n.doubleValue();
????return total;
}
注意,這樣的話,那些類型轉(zhuǎn)換已經(jīng)不再需要了。我們并不知道list中元素的具體類型,但是我們知道他們能夠向上轉(zhuǎn)型為Number,因此我們可以把他們從list中把他們當(dāng)作一個(gè)Number對(duì)象取出。使用一個(gè)for/in循環(huán)能夠稍微封裝一下從list中取出元素的過(guò)程。普遍性的原則是當(dāng)您使用一個(gè)界定通配符時(shí),類似于List中的get()方法的那些方法將返回一個(gè)類型為上界的值。因此如果我們?cè)趂or/in循環(huán)中調(diào)用list.get(),我們將得到一個(gè)Number。在前一節(jié)說(shuō)到使用通配符時(shí)類似于list.add()這種方法中的限制依然有效:舉個(gè)例子來(lái)說(shuō),如果編譯器允許我們調(diào)用這類方法,我們就可以將一個(gè)Integer放到一個(gè)聲明為僅保持Short值的list中去。
同樣可行的是使用下界通配符,不同的是用super替換extends。這個(gè)技巧在被調(diào)用的方法上有一點(diǎn)不同的作用。在實(shí)際應(yīng)用中,下界通配符要比上界通配符用得少。我們將在后面的章節(jié)里討論這個(gè)問(wèn)題。