表面上看起來,無論語法還是應(yīng)用的環(huán)境(比如容器類),泛型類型(或者泛型)都類似于 C++ 中的模板。但是這種相似性僅限于表面,Java 語言中的泛型基本上完全在編譯器中實(shí)現(xiàn),由編譯器執(zhí)行類型檢查和類型推斷,然后生成普通的非泛型的字節(jié)碼。這種實(shí)現(xiàn)技術(shù)稱為 擦除(erasure) (編譯器使用泛型類型信息保證類型安全,然后在生成字節(jié)碼之前將其清除),這項(xiàng)技術(shù)有一些奇怪,并且有時(shí)會(huì)帶來一些令人迷惑的后果。雖然范型是 Java 類走向類型安全的一大步,但是在學(xué)習(xí)使用泛型的過程中幾乎肯定會(huì)遇到頭痛(有時(shí)候讓人無法忍受)的問題。
注意: 本文假設(shè)您對 JDK 5.0 中的范型有基本的了解。
雖然將集合看作是數(shù)組的抽象會(huì)有所幫助,但是數(shù)組還有一些集合不具備的特殊性質(zhì)。Java 語言中的數(shù)組是協(xié)變的(covariant),也就是說,如果
Integer
擴(kuò)展了
Number
(事實(shí)也是如此),那么不僅
Integer
是
Number
,而且
Integer[]
也是
Number[]
,在要求
Number[]
的地方完全可以傳遞或者賦予
Integer[]
。(更正式地說,如果
Number
是
Integer
的超類型,那么
Number[]
也是
Integer[]
的超類型)。您也許認(rèn)為這一原理同樣適用于泛型類型 ——
List<Number>
是
List<Integer>
的超類型,那么可以在需要
List<Number>
的地方傳遞
List<Integer>
。不幸的是,情況并非如此。
不允許這樣做有一個(gè)很充分的理由:這樣做將破壞要提供的類型安全泛型。如果能夠?qū)?
List<Integer>
賦給
List<Number>
。那么下面的代碼就允許將非
Integer
的內(nèi)容放入
List<Integer>
:
List<Integer> li = new ArrayList<Integer>(); |
因?yàn)?
ln
是
List<Number>
,所以向其添加
Float
似乎是完全合法的。但是如果
ln
是
li
的別名,那么這就破壞了蘊(yùn)含在
li
定義中的類型安全承諾 —— 它是一個(gè)整數(shù)列表,這就是泛型類型不能協(xié)變的原因。
數(shù)組能夠協(xié)變而泛型不能協(xié)變的另一個(gè)后果是,不能實(shí)例化泛型類型的數(shù)組(
new List<String>[3]
是不合法的),除非類型參數(shù)是一個(gè)未綁定的通配符(
new List<?>[3]
是合法的)。讓我們看看如果允許聲明泛型類型數(shù)組會(huì)造成什么后果:
List<String>[] lsa = new List<String>[10]; // illegal |
最后一行將拋出
ClassCastException
,因?yàn)檫@樣將把
List<Integer>
填入本應(yīng)是
List<String>
的位置。因?yàn)閿?shù)組協(xié)變會(huì)破壞泛型的類型安全,所以不允許實(shí)例化泛型類型的數(shù)組(除非類型參數(shù)是未綁定的通配符,比如
List<?>
)。
因?yàn)榭梢圆脸δ埽?
List<Integer>
和
List<String>
是同一個(gè)類,編譯器在編譯
List<V>
時(shí)只生成一個(gè)類(和 C++ 不同)。因此,在編譯
List<V>
類時(shí),編譯器不知道
V
所表示的類型,所以它就不能像知道類所表示的具體類型那樣處理
List<V>
類定義中的類型參數(shù)(
List<V>
中的
V
)。
因?yàn)檫\(yùn)行時(shí)不能區(qū)分
List<String>
和
List<Integer>
(運(yùn)行時(shí)都是
List
),用泛型類型參數(shù)標(biāo)識(shí)類型的變量的構(gòu)造就成了問題。運(yùn)行時(shí)缺乏類型信息,這給泛型容器類和希望創(chuàng)建保護(hù)性副本的泛型類提出了難題。
比如泛型類
Foo
:
class Foo<T> { |
假設(shè)
doSomething()
方法希望復(fù)制輸入的
param
參數(shù),會(huì)怎么樣呢?沒有多少選擇。您可能希望按以下方式實(shí)現(xiàn)
doSomething()
:
public void doSomething(T param) { |
但是您不能使用類型參數(shù)訪問構(gòu)造函數(shù),因?yàn)樵诰幾g的時(shí)候還不知道要構(gòu)造什么類,因此也就不知道使用什么構(gòu)造函數(shù)。使用泛型不能表達(dá)“
T
必須擁有一個(gè)拷貝構(gòu)造函數(shù)(copy constructor)”(甚至一個(gè)無參數(shù)的構(gòu)造函數(shù))這類約束,因此不能使用泛型類型參數(shù)所表示的類的構(gòu)造函數(shù)。
clone()
怎么樣呢?假設(shè)在
Foo
的定義中,
T
擴(kuò)展了
Cloneable
:
class Foo<T extends Cloneable> { |
不幸的是,仍然不能調(diào)用
param.clone()
。為什么呢?因?yàn)?
clone()
在
Object
中是保護(hù)訪問的,調(diào)用
clone()
必須通過將
clone()
改寫公共訪問的類引用來完成。但是重新聲明
clone()
為 public 并不知道
T
,因此克隆也無濟(jì)于事。
因此,不能復(fù)制在編譯時(shí)根本不知道是什么類的類型引用。那么使用通配符類型怎么樣?假設(shè)要?jiǎng)?chuàng)建類型為
Set<?>
的參數(shù)的保護(hù)性副本。您知道
Set
有一個(gè)拷貝構(gòu)造函數(shù)。而且別人可能曾經(jīng)告訴過您,如果不知道要設(shè)置的內(nèi)容的類型,最好使用
Set<?>
代替原始類型的
Set
,因?yàn)檫@種方法引起的未檢查類型轉(zhuǎn)換警告更少。于是,可以試著這樣寫:
class Foo { |
不幸的是,您不能用通配符類型的參數(shù)調(diào)用泛型構(gòu)造函數(shù),即使知道存在這樣的構(gòu)造函數(shù)也不行。不過您可以這樣做:
class Foo { |
這種構(gòu)造不那么直觀,但它是類型安全的,而且可以像
new HashSet<?>(set)
那樣工作。
如何實(shí)現(xiàn)
ArrayList<V>
?假設(shè)類
ArrayList
管理一個(gè)
V
數(shù)組,您可能希望用
ArrayList<V>
的構(gòu)造函數(shù)創(chuàng)建一個(gè)
V
數(shù)組:
class ArrayList<V> { |
但是這段代碼不能工作 —— 不能實(shí)例化用類型參數(shù)表示的類型數(shù)組。編譯器不知道
V
到底表示什么類型,因此不能實(shí)例化
V
數(shù)組。
Collections 類通過一種別扭的方法繞過了這個(gè)問題,在 Collections 類編譯時(shí)會(huì)產(chǎn)生類型未檢查轉(zhuǎn)換的警告。
ArrayList
具體實(shí)現(xiàn)的構(gòu)造函數(shù)如下:
class ArrayList<V> { |
為何這些代碼在訪問
backingArray
時(shí)沒有產(chǎn)生
ArrayStoreException
呢?無論如何,都不能將
Object
數(shù)組賦給
String
數(shù)組。因?yàn)榉盒褪峭ㄟ^擦除實(shí)現(xiàn)的,
backingArray
的類型實(shí)際上就是
Object[]
,因?yàn)?
Object
代替了
V
。這意味著:實(shí)際上這個(gè)類期望
backingArray
是一個(gè)
Object
數(shù)組,但是編譯器要進(jìn)行額外的類型檢查,以確保它包含
V
類型的對象。所以這種方法很奏效,但是非常別扭,因此不值得效仿(甚至連泛型 Collections 框架的作者都這么說,請參閱
參考資料
)。
還有一種方法就是聲明
backingArray
為
Object
數(shù)組,并在使用它的各個(gè)地方強(qiáng)制將它轉(zhuǎn)化為
V[]
。仍然會(huì)看到類型未檢查轉(zhuǎn)換警告(與上一種方法一樣),但是它使一些未明確的假設(shè)更清楚了(比如
backingArray
不應(yīng)逃避
ArrayList
的實(shí)現(xiàn))。
最好的辦法是向構(gòu)造函數(shù)傳遞類文字(
Foo.class
),這樣,該實(shí)現(xiàn)就能在運(yùn)行時(shí)知道
T
的值。不采用這種方法的原因在于向后兼容性 —— 新的泛型集合類不能與 Collections 框架以前的版本兼容。
下面的代碼中
ArrayList
采用了以下方法:
public class ArrayList<V> implements List<V> { |
但是等一等!仍然有不妥的地方,調(diào)用
Array.newInstance()
時(shí)會(huì)引起未經(jīng)檢查的類型轉(zhuǎn)換。為什么呢?同樣是由于向后兼容性。
Array.newInstance()
的簽名是:
public static Object newInstance(Class<?> componentType, int length) |
而不是類型安全的:
public static<T> T[] newInstance(Class<T> componentType, int length) |
為何
Array
用這種方式進(jìn)行泛化呢?同樣是為了保持向后兼容。要?jiǎng)?chuàng)建基本類型的數(shù)組,如
int[]
,可以使用適當(dāng)?shù)陌b器類中的
TYPE
字段調(diào)用
Array.newInstance()
(對于
int
,可以傳遞
Integer.TYPE
作為類文字)。用
Class<T>
參數(shù)而不是
Class<?>
泛化
Array.newInstance()
,對于引用類型有更好的類型安全,但是就不能使用
Array.newInstance()
創(chuàng)建基本類型數(shù)組的實(shí)例了。也許將來會(huì)為引用類型提供新的
newInstance()
版本,這樣就兩者兼顧了。
在這里可以看到一種模式 —— 與泛型有關(guān)的很多問題或者折衷并非來自泛型本身,而是保持和已有代碼兼容的要求帶來的副作用。
![]() ![]() |
![]()
|
在轉(zhuǎn)化現(xiàn)有的庫類來使用泛型方面沒有多少技巧,但與平常的情況相同,向后兼容性不會(huì)憑空而來。我已經(jīng)討論了兩個(gè)例子,其中向后兼容性限制了類庫的泛化。
另一種不同的泛化方法可能不存在向后兼容問題,這就是
Collections.toArray(Object[])
。傳入
toArray()
的數(shù)組有兩個(gè)目的 —— 如果集合足夠小,那么可以將其內(nèi)容直接放在提供的數(shù)組中。否則,利用反射(reflection)創(chuàng)建相同類型的新數(shù)組來接受結(jié)果。如果從頭開始重寫 Collections 框架,那么很可能傳遞給
Collections.toArray()
的參數(shù)不是一個(gè)數(shù)組,而是一個(gè)類文字:
interface Collection<E> { |
因?yàn)?Collections 框架作為良好類設(shè)計(jì)的例子被廣泛效仿,但是它的設(shè)計(jì)受到向后兼容性約束,所以這些地方值得您注意,不要盲目效仿。
首先,常常被混淆的泛型 Collections API 的一個(gè)重要方面是
containsAll()
、
removeAll()
和
retainAll()
的簽名。您可能認(rèn)為
remove()
和
removeAll()
的簽名應(yīng)該是:
interface Collection<E> { |
但實(shí)際上卻是:
interface Collection<E> { |
為什么呢?答案同樣是因?yàn)橄蚝蠹嫒菪浴?
x.remove(o)
的接口表明“如果
o
包含在
x
中,則刪除它,否則什么也不做。”如果
x
是一個(gè)泛型集合,那么
o
不一定與
x
的類型參數(shù)兼容。如果
removeAll()
被泛化為只有類型兼容時(shí)才能調(diào)用(
Collection<? extends E>
),那么在泛化之前,合法的代碼序列就會(huì)變得不合法,比如:
// a collection of Integers |
如果上述片段用直觀的方法泛化(將
c
設(shè)為
Collection<Integer>
,
r
設(shè)為
Collection<Object>
),如果
removeAll()
的簽名要求其參數(shù)為
Collection<? extends E>
而不是 no-op,那么就無法編譯上面的代碼。泛型類庫的一個(gè)主要目標(biāo)就是不打破或者改變已有代碼的語義,因此,必須用比從頭重新設(shè)計(jì)泛型所使用類型約束更弱的類型約束來定義
remove()
、
removeAll()
、
retainAll()
和
containsAll()
。
在泛型之前設(shè)計(jì)的類可能阻礙了“顯然的”泛型化方法。這種情況下就要像上例這樣進(jìn)行折衷,但是如果從頭設(shè)計(jì)新的泛型類,理解 Java 類庫中的哪些東西是向后兼容的結(jié)果很有意義,這樣可以避免不適當(dāng)?shù)哪7隆?
![]() ![]() |
![]()
|
因?yàn)榉盒突旧隙际窃?Java 編譯器中而不是運(yùn)行庫中實(shí)現(xiàn)的,所以在生成字節(jié)碼的時(shí)候,差不多所有關(guān)于泛型類型的類型信息都被“擦掉”了。換句話說,編譯器生成的代碼與您手工編寫的不 用泛型、檢查程序的類型安全后進(jìn)行強(qiáng)制類型轉(zhuǎn)換所得到的代碼基本相同。與 C++ 不同,
List<Integer>
和
List<String>
是同一個(gè)類(雖然是不同的類型但都是
List<?>
的子類型,與以前的版本相比,在 JDK 5.0 中這是一個(gè)更重要的區(qū)別)。
擦除意味著一個(gè)類不能同時(shí)實(shí)現(xiàn)
Comparable<String>
和
Comparable<Number>
,因?yàn)槭聦?shí)上兩者都在同一個(gè)接口中,指定同一個(gè)
compareTo()
方法。聲明
DecimalString
類以便與
String
與
Number
比較似乎是明智的,但對于 Java 編譯器來說,這相當(dāng)于對同一個(gè)方法進(jìn)行了兩次聲明:
public class DecimalString implements Comparable<Number>, Comparable<String> { ... } // nope |
擦除的另一個(gè)后果是,對泛型類型參數(shù)是用強(qiáng)制類型轉(zhuǎn)換或者
instanceof
毫無意義。下面的代碼完全不會(huì)改善代碼的類型安全性:
public <T> T naiveCast(T t, Object o) { return (T) o; } |
編譯器僅僅發(fā)出一個(gè)類型未檢查轉(zhuǎn)換警告,因?yàn)樗恢肋@種轉(zhuǎn)換是否安全。
naiveCast()
方法實(shí)際上根本不作任何轉(zhuǎn)換,
T
直接被替換為
Object
,與期望的相反,傳入的對象被強(qiáng)制轉(zhuǎn)換為
Object
。
擦除也是造成上述構(gòu)造問題的原因,即不能創(chuàng)建泛型類型的對象,因?yàn)榫幾g器不知道要調(diào)用什么構(gòu)造函數(shù)。如果泛型類需要構(gòu)造用泛型類型參數(shù)來指定類型的對象,那么構(gòu)造函數(shù)應(yīng)該接受類文字(
Foo.class
)并將它們保存起來,以便通過反射創(chuàng)建實(shí)例。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
