開發(fā)人員在使用泛型的時(shí)候,很容易根據(jù)自己的直覺而犯一些錯(cuò)誤,比如一個(gè)方法如果接收List<Object>作為形式參數(shù),那么如果嘗試將一個(gè)List<String>的對(duì)象作為實(shí)際參數(shù)傳進(jìn)去,卻發(fā)現(xiàn)無(wú)法通過編譯驹溃。雖然從直覺來說,Object是String的父類儒搭,這個(gè)類型轉(zhuǎn)換應(yīng)該是合理的吠架。 ** 但是實(shí)際上這會(huì)產(chǎn)生隱含的類型轉(zhuǎn)換問題,因此編譯器直接就禁止這樣的行為 ** 搂鲫。
類型擦除
java中的泛型基本上都在編譯器這個(gè)層次來實(shí)現(xiàn)的傍药,** 在生成的java字節(jié)代碼中是不包含泛型中的數(shù)據(jù)類型的。使用泛型的時(shí)候加上的類型參數(shù)魂仍,會(huì)被編譯器在編譯的時(shí)候去掉拐辽,這個(gè)過程稱為類型擦除 ** 。如在代碼中定義的List<Object>和List<String>等類型擦酌,在編譯之后都會(huì)變成List俱诸。 ** JVM看到的只是list,而由泛型附加的類型信息對(duì)JVM來說是不可以見的 ** 赊舶。java編譯器會(huì)在編譯時(shí)盡可能發(fā)現(xiàn)可能出錯(cuò)的地方睁搭,但是仍然無(wú)法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。
很多泛型的奇怪特性都與這個(gè)類型擦除的存在有關(guān)笼平,包括:
- ** 泛型類并沒有自己獨(dú)有的class類對(duì)象 ** 园骆。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
- ** 靜態(tài)變量是被泛型類的所有實(shí)例所共享的 ** 寓调。對(duì)于聲明為MyClass<T>的類锌唾,訪問其中的靜態(tài)變量的方法仍然是MyClass.myStaticVar。不管是通過 new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對(duì)象,都是共享一個(gè)靜態(tài)變量晌涕。
- ** 泛型的類型參數(shù)不能用在java異常處理的catch語(yǔ)句中滋捶,因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時(shí)刻來進(jìn)行的 ** 。由于類型信息被擦除余黎,JVM是無(wú)法區(qū)分兩個(gè)異常類型MyException<String>和MyException<Integer>的重窟。對(duì)于JVM來說,它們都的MyException類型的惧财。也就是無(wú)法執(zhí)行與異常對(duì)應(yīng)的catch語(yǔ)句亲族。
類型擦除的基本過程也比較簡(jiǎn)單,首先是找到用來替換類型參數(shù)的具體類可缚。這個(gè)具體類一般是Object。如果指定了類型參數(shù)的上界的話斋枢,則使用這個(gè)上界帘靡。把代碼中的類型參數(shù)都替換成具體的類。同時(shí)去掉出現(xiàn)的類型聲明瓤帚,即去掉<>的內(nèi)容描姚。比如 T get()方法聲明就變成了Object get();List<String>就變成了List。接下來就可能需要生成一些橋接方法(bridge method)戈次。這是由于擦除了類型之后的類可能缺少某些必須的方法轩勘。比如考慮下面的代碼:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
當(dāng)類型信息被擦除之后,上述類的聲明變成了class MyString implements Comparable怯邪。但是這樣的話绊寻,類MyString就會(huì)有編譯錯(cuò)誤,因?yàn)闆]有實(shí)現(xiàn)接口Comparable聲明的int compareTo(Object)方法悬秉。這個(gè)時(shí)候就由編譯器來動(dòng)態(tài)生成這個(gè)方法澄步。
通配符
在使用泛型類的時(shí)候,既可以指定一個(gè)具體的類型和泌,如List<String>就聲明了具體的類型是String;也可以用通配符?來表示未知類型村缸,如List<?>就聲明了List中包含的元素類型是未知的。通配符所代表的其實(shí)是一組類型武氓,但具體的類型是未知的梯皿。List<?>所聲明的就是所有類型都是可以的。 ** 但是List <?> 并不等同于List<Object>县恕。List<Object>實(shí)際上確定了List中包含的是Object及子類东羹,在使用的時(shí)候都可以通過Object來進(jìn)行引用。而List<?>則其中包含的元素類型是不確定 **弱睦。其中可能包含的是String,也可能是Integer百姓。如果它包含了String的話,往里面添加Integer類型的元素就是錯(cuò)誤的况木, ** 正因?yàn)轭愋臀粗萋#筒荒芡ㄟ^new ArrayList<?>()方法來創(chuàng)建一個(gè)新的ArrayList對(duì)象旬迹,因?yàn)榫幾g器無(wú)法知道具體的類型是什么。但是對(duì)于List<?>中的元素總是可以用Object來引用的求类,因?yàn)殡m然類型未知奔垦,但肯定是Object及其子類 ** ∈考慮下面的代碼:
public void wildcard(List<?> list) {
list.add(1);//編譯錯(cuò)誤
}
如上所示椿猎,試圖對(duì)一個(gè)帶通配符的泛型類進(jìn)行操作的時(shí)候,總是會(huì)出現(xiàn)編譯錯(cuò)誤寿弱,其它原因在于通配符所表示的類型是未知的犯眠。
因?yàn)閷?duì)于List<?>中的元素只能用Object來引用,在有些情況下不是很方便症革。在這些情況下筐咧,可以使用上下界來限制未知類型的范圍。如List <? extends Number>說明List中可能包含的元素類型是Number及其子類噪矛。而List <? super Number>則說明List中包含的是Number及其父類量蕊。當(dāng)引入了上界之后,在使用類型的時(shí)候就可以使用上界類中定義的方法艇挨。
類型系統(tǒng)
在java中残炮,大家比較熟悉的是通過繼承機(jī)制而產(chǎn)生的類型體系結(jié)構(gòu)。比如String繼承自O(shè)bject缩滨。根據(jù)Listov替換原則势就,子類是可以替換父類的。當(dāng)需要Object類的引用的時(shí)候脉漏,如果傳入一個(gè)String對(duì)象是沒有任何問題的蛋勺。但是反過來的話,即用父類的引用替換子類引用的時(shí)候鸠删,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換抱完。編譯器并不能保證運(yùn)行時(shí)刻這種轉(zhuǎn)換一定是合法的。** 這種自動(dòng)的子類替換父類的類型轉(zhuǎn)換機(jī)制刃泡,對(duì)于數(shù)組也是適用的巧娱。String[]可以替換Object[] ** 。但是泛型的引入烘贴,對(duì)于這個(gè)類型系統(tǒng)產(chǎn)生了一定的影響禁添。 ** 正始前面提到的list是不是能替換掉List的。引入泛型之后的類型系統(tǒng)增加了兩個(gè)維度:一個(gè)是類型參數(shù)自身的繼承體系結(jié)構(gòu)桨踪,另外一個(gè)是泛型類或接口自身的繼承體系結(jié)構(gòu)老翘。第一個(gè)指的是對(duì)于List<String>和List<Object>這樣的情況。類型String是繼承自O(shè)bject的。而第二種指的是List接口繼承自Collection接口铺峭。對(duì)于這個(gè)類型的系統(tǒng)墓怀,有如下的一些規(guī)則: **
- 相同類型參數(shù)的泛型的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)。即List<String>是Collection<Sting>的子類型卫键,List<String>可以替換Collection<String>傀履。這種情況也適用于帶有上下界的類型聲明。
- 當(dāng)泛型類的類型聲明中使用了通配符的時(shí)候莉炉,其子類型可以在兩個(gè)維度上分別展開钓账。如對(duì)Collection <? extends Number>來說絮宁,其子類型可以在Collection這個(gè)維度上展開梆暮,即List <? extends Number>和Set<? extends Number>等;也可以在Number這個(gè)層次上展開,即Collection<Double>和Collection<Integer>等绍昂。如此循環(huán)下去惕蹄,ArrayList<Long>和HashSet<Double>等也都算是Collection <? extends Number>的子類型。
- 如果泛型類中包含多個(gè)類型參數(shù)治专,則對(duì)于每個(gè)類型參數(shù)分別應(yīng)用上面的規(guī)則。