泛型程序設(shè)計(jì)
泛型程序設(shè)計(jì)意味著編寫的代碼可以被很多不同類型的對(duì)象所重用。例如ArrayList類可以聚集任何類型的對(duì)象践美,這是一個(gè)泛型程序設(shè)計(jì)的實(shí)例股毫。實(shí)際上,在Java增加泛型類之前已經(jīng)有一個(gè)ArrayList類达皿,下面來(lái)研究泛型程序設(shè)計(jì)機(jī)制是如何演變的。
類型參數(shù)的好處
在Java增加泛型類之前贿肩,泛型程序設(shè)計(jì)是用繼承實(shí)現(xiàn)的峦椰。ArrayList類只維護(hù)一個(gè)Object引用的數(shù)組:
public class ArrayList
{
private Object[] elementData;
. . .
public Object get(int i) { . . . }
public void add(Object o) { . . . }
}
這種方法有兩個(gè)問(wèn)題,我們以一個(gè)保存文件名的files數(shù)組列表為例:
- 當(dāng)獲取一個(gè)值時(shí)必須進(jìn)行強(qiáng)制類型轉(zhuǎn)換:
ArrayList files = new ArrayList();
. . .
String filename = (String)files.get(0);
- 當(dāng)添加一個(gè)值時(shí)不進(jìn)行類型檢查汰规,可以添加任何類的對(duì)象:
files.add(new File(". . ."));
此調(diào)用在編譯和運(yùn)行時(shí)都不會(huì)出錯(cuò)汤功,但如果將get的結(jié)果強(qiáng)制轉(zhuǎn)換為String會(huì)產(chǎn)生一個(gè)錯(cuò)誤
為了解決上述問(wèn)題,引入了類型參數(shù)溜哮。ArrayList有一個(gè)類型參數(shù)用來(lái)指示元素類型:ArrayList<T> files = new ArrayList<T>();
滔金,注意前后兩個(gè)T必須一致,不能是子類和父類茂嗓!因?yàn)樵诜盒椭胁鸵穑绻鸄是B的子類,泛型類<A>和泛型類<B>之間并沒(méi)有任何的關(guān)系述吸。在Java SE 7及以后的版本忿族,構(gòu)造函數(shù)可以省略類型參數(shù),即ArrayList<T> files = new ArrayList<>();
這種方法有三個(gè)好處:
1)get得到的值不必進(jìn)行強(qiáng)制類型轉(zhuǎn)換
2)向數(shù)組列表中添加對(duì)象時(shí)會(huì)進(jìn)行類型檢查蝌矛,如果不符合類型肠阱,無(wú)法通過(guò)編譯,出現(xiàn)編譯錯(cuò)誤比類在運(yùn)行時(shí)出現(xiàn)類的強(qiáng)制轉(zhuǎn)換異常要好得多
3)程序具有更好的可讀性朴读,比如ArrayList<String> files
一看就是聚集了String對(duì)象的數(shù)組列表
定義簡(jiǎn)單的泛型類
一個(gè)泛型類就是具有一個(gè)或多個(gè)類型變量的類屹徘,下面使用一個(gè)簡(jiǎn)單的Pair類作為例子:
public class Pair<T>
{
private T first;
private T second;
public Pair() {first = null; second = null}
public Pair(T first,T second) {this.first = first; this.second = second}
public void setFirst(T newValue) {first = newValue;}
public void setSecond(T newValue) {second = newValue;}
}
Pair類引入了一個(gè)類型變量T,用尖括號(hào)括起來(lái)衅金,并放在類名的后面噪伊。注意這里的T只是指定了一種類型,不代表類只有一個(gè)屬性成員氮唯,而表示接下來(lái)類中出現(xiàn)的T都和尖括號(hào)里的類型保持一致,即類定義的類型變量指定方法返回類型以及域和局部變量的類型鉴吹。
泛型類可以有多個(gè)類型變量,如可以定義兩個(gè)域類型不同的Pair類public class Pair<T,U> {. . .}
類型變量命名規(guī)則:E表示集合的元素類型惩琉,K和V分別表示表的鍵和值的類型豆励,T(需要時(shí)還可以用臨近的字母U和S)表示“任意類型”。
實(shí)際使用中將類定義尖括號(hào)里的T替換為具體類型,類中的T也會(huì)進(jìn)行相應(yīng)的替換良蒸,所以技扼,泛型類可以看作普通類的工廠
泛型方法
前面介紹了如何定義一個(gè)泛型類,下面我們來(lái)定義一個(gè)帶有類型參數(shù)的簡(jiǎn)單泛型方法:
class ArrayAlg
{
public static <T> T getMiddle(T...a)
{
return a[a.length / 2];
}
}
注意:
1)類型變量放在修飾符(這里是public static)的后面嫩痰,返回類型的前面
2)泛型方法可以定義在普通類中剿吻,也可以定義在泛型類中
3)當(dāng)調(diào)用一個(gè)泛型方法時(shí),在方法名前的尖括號(hào)中放入具體的類型:
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
或者在Java1.7/1.8利用type inference串纺,讓Java自動(dòng)推導(dǎo)出相應(yīng)的類型參數(shù):
String middle = ArrayAlg.getMiddle("John","Q.","Public");
我們注意到上面的泛型方法的參數(shù)必須是T類型丽旅,如果想要傳入T類型的子類怎么辦呢?我們可以把<T>
改為<? extends T>
纺棺,這樣參數(shù)就可以接受T類型以及它的子類榄笙。類似的<? super T>
則表示參數(shù)可以為T類型以及它的超類。以上統(tǒng)稱為通配符祷蝌。
類型變量的限定
有時(shí)茅撞,類或方法需要對(duì)類型變量加以約束。下面是一個(gè)典型的例子杆逗,我們要計(jì)算數(shù)組中的最小元素:
class ArrayAlg
{
public static <T> T min(T[] a)
{
if(a == null || a.length == 0) return null;
T smallest = a[0];
for(int i = 1; i < a.length;i++)
if(smallest.compareTo(a[i]) > 0)
smallest = a[i];
return smallset;
}
}
我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題乡翅,smallest的類型為T鳞疲,既可以是任何一個(gè)類的對(duì)象罪郊,如何保證T類型有compareTo方法呢?解決方法是限制T為實(shí)現(xiàn)了Comparable接口的類尚洽,即將泛型方法定義改為public static <T extends Comparable> T min(T[] a)
注意:
1)限定類時(shí)如果實(shí)現(xiàn)的是接口也用extends關(guān)鍵字悔橄,如上面的T extends Comparable
2)只需要在尖括號(hào)里限定,之后的T不用再加限定
一個(gè)類型變量或通配符可以有多個(gè)限定腺毫,用'&'分隔限定類型癣疟,用逗號(hào)分隔類型變量,例如:T extends Comparable & Serializable
可以有多個(gè)接口限定和至多一個(gè)類限定潮酒,將標(biāo)記接口(空接口)放在限定列表的末尾睛挚,如果用一個(gè)類作為限定,它必須是限定列表中的第一個(gè)急黎。
這種限定方式稱為邊界符扎狱。
PECS原則
上面我們看到了類似<? extends T>的用法,利用它我們可以從list里面get元素勃教,那么我們可不可以往list里面add元素呢淤击?我們來(lái)嘗試一下:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple())
// flist.add(new Orange())
// flist.add(new Fruit())
// flist.add(new Object())
flist.add(null); // Legal but uninteresting
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
答案是否定,Java編譯器不允許我們這樣做故源,為什么呢污抬?對(duì)于這個(gè)問(wèn)題我們不妨從編譯器的角度去考慮。因?yàn)長(zhǎng)ist<? extends Fruit> flist它自身可以有多種含義:
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
- 當(dāng)我們嘗試add一個(gè)Apple的時(shí)候绳军,flist可能指向new ArrayList<Orange>();
- 當(dāng)我們嘗試add一個(gè)Orange的時(shí)候印机,flist可能指向new ArrayList<Apple>();
- 當(dāng)我們嘗試add一個(gè)Fruit的時(shí)候矢腻,這個(gè)Fruit可以是任何類型的Fruit,而flist可能只想某種特定類型的Fruit耳贬,編譯器無(wú)法識(shí)別所以會(huì)報(bào)錯(cuò)踏堡。
所以對(duì)于實(shí)現(xiàn)了<? extends T>的集合類只能將它視為Producer向外提供(get)元素,而不能作為Consumer來(lái)從外獲取(add)元素咒劲。
如果我們要add元素應(yīng)該怎么做呢顷蟆?可以使用<? super T>:
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static void f1() {
writeExact(apples, new Apple());
writeExact(fruit, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item)
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) {
f1(); f2();
}
}
這樣我們可以往容器里面添加元素了,但是使用super的壞處是以后不能get容器里面的元素了腐魂,原因很簡(jiǎn)單蹬屹,我們繼續(xù)從編譯器的角度考慮這個(gè)問(wèn)題,對(duì)于List<? super Apple> list膏燃,它可以有下面幾種含義:
List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();
當(dāng)我們嘗試通過(guò)list來(lái)get一個(gè)Apple的時(shí)候滥搭,可能會(huì)get得到一個(gè)Fruit,這個(gè)Fruit可以是Orange等其他類型的Fruit兔毒。
根據(jù)上面的例子漫贞,我們可以總結(jié)出一條規(guī)律,”Producer Extends, Consumer Super”:
- “Producer Extends” – 如果你需要一個(gè)只讀List育叁,用它來(lái)produce T迅脐,那么使用? extends T。
- “Consumer Super” – 如果你需要一個(gè)只寫List豪嗽,用它來(lái)consume T谴蔑,那么使用? super T。
- 如果需要同時(shí)讀取以及寫入龟梦,那么我們就不能使用通配符了隐锭。
如何閱讀過(guò)一些Java集合類的源碼,可以發(fā)現(xiàn)通常我們會(huì)將兩者結(jié)合起來(lái)一起用计贰,比如像下面這樣:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
類型擦除
對(duì)于虛擬機(jī)來(lái)說(shuō)沒(méi)有泛型類型钦睡,只有普通類和方法。無(wú)論何時(shí)定義一個(gè)泛型類型躁倒,都自動(dòng)提供了一個(gè)相應(yīng)的原始類型荞怒。類型擦除就是說(shuō)Java泛型只能用于在編譯期間的靜態(tài)類型檢查,然后編譯器生成的代碼會(huì)擦除相應(yīng)的類型信息樱溉,泛型類型會(huì)被轉(zhuǎn)化為原始類型挣输,這樣到了運(yùn)行期間實(shí)際上JVM根本就不知道泛型所代表的具體類型。原始類型就是刪去類型參數(shù)后的泛型類型名福贞。擦除類型變量撩嚼,并替換為限定類型(無(wú)限定類型的變量替換為Object)。
例如,Pair<T>
的原始類型如下:
public class Pair
{
private Object first;
private Object second;
public Pair() {first = null; second = null}
public Pair(Object first,Object second) {this.first = first; this.second = second}
public void setFirst(Object newValue) {first = newValue;}
public void setSecond(Object newValue) {second = newValue;}
}
因?yàn)門是一個(gè)無(wú)限定的參數(shù)變量完丽,所以直接用Object替換
如果是T是一個(gè)限定類型的參數(shù)變量恋技,用限定列表中的第一個(gè)類型替換,比如T extends Comparable & Serializable
被替換為Comparable
翻譯泛型表達(dá)式
當(dāng)泛型表達(dá)式編譯時(shí)逻族,編譯器會(huì)將其編譯為原始類型并加入強(qiáng)制類型轉(zhuǎn)換蜻底。比如:
Pair<Employee> buddies = . . . ;
Employee buddy = buddies.getFirst();
編譯器會(huì)把這個(gè)方法調(diào)用翻譯為兩條虛擬機(jī)指令:
- 對(duì)原始方法Pair.getFirst的調(diào)用
- 將返回的Object類型強(qiáng)制轉(zhuǎn)換為Employee類型
當(dāng)存入一個(gè)泛型域時(shí)編譯器也會(huì)自動(dòng)在字節(jié)碼中插入強(qiáng)制類型轉(zhuǎn)換。
翻譯泛型方法
類型擦除會(huì)發(fā)生一些問(wèn)題聘鳞,比如一個(gè)類subPair繼承自類Pair<String>
:
class SubPair extends Pair<String> //繼承一個(gè)泛型類
{
public void setFirst(String newValue){....}//重寫了父類的方法
}
Pair在編譯的時(shí)候被類型擦除薄辅,Pair的setFirst方法變?yōu)榱藄etFirst(Object newValue),這樣SubPair的setFirst(Stirng newValue)方法就無(wú)法覆蓋父類中的setFirst(Object newValue)方法抠璃,因?yàn)閰?shù)不同站楚,不是同一個(gè)方法。
SubPair中會(huì)出現(xiàn)兩個(gè)方法:
public void setFirst(String newValue){....}
public void setFirst(Object newValue){....}
此時(shí)如果有如下測(cè)試代碼:
SubPair p = new SubPair(. . .);
Pair<String> pair = p;
p.setFirst("Hello world");
按理來(lái)說(shuō)搏嗡,這段測(cè)試代碼應(yīng)該不能通過(guò)編譯窿春,因?yàn)橐獙?shí)現(xiàn)多態(tài)的話,所調(diào)用的方法必須在子類中重寫采盒,但是在這里SubPair類并沒(méi)有重寫Pair類中的setFirst方法旧乞,只是單純的繼承而已,并且新加了一個(gè)參數(shù)不同的同名方法磅氨。
但結(jié)果可以運(yùn)行尺栖,因?yàn)闉榱私鉀Q這種類型擦除和多態(tài)的沖突,編譯器會(huì)在SubPair類中生成一個(gè)橋方法悍赢,上述兩個(gè)方法被編譯器變成了這樣:
public void setFirst(String newValue){....} //子類定義的方法不變
//編譯器生成的橋方法
public void setFirst(Object newValue)
{
setFirst((String) newValue);
//橋內(nèi)部調(diào)用的是子類中定義的setFirst(String newValue)方法
}
可以看出决瞳,這個(gè)橋方法實(shí)際上就是對(duì)超類中setFirst(Object newValue)的重寫货徙。這樣做的原因是左权,當(dāng)程序員在子類中寫下setFirst(String newValue)方法時(shí),本意是對(duì)超類中的同名方法進(jìn)行重寫痴颊,但因?yàn)槌惏l(fā)生了類型擦除赏迟,所以實(shí)際上并沒(méi)有重寫成功,因此加入了橋方法的機(jī)制來(lái)避免類型擦除與多態(tài)發(fā)生沖突蠢棱。
詳見(jiàn)博客:Java中的類型擦除與橋方法
約束與局限性
在使用Java泛型時(shí)需要考慮一些限制锌杀。大多數(shù)限制都是由類型擦除引起的。
1.不能用基本類型實(shí)例化類型參數(shù)
類型參數(shù)不能為基本類型泻仙。比如糕再,沒(méi)有Pair<double>
只有Pair<Double>
,其原因是類型擦除,因?yàn)椴脸笥褡琍air類含有Object類型的域突想,Object不能存儲(chǔ)double值。
2.運(yùn)行時(shí)類型查詢只適用于原始類型
使用instanceof查詢一個(gè)對(duì)象是否屬于某個(gè)泛型類會(huì)產(chǎn)生編譯錯(cuò)誤,比如:
if(a instanceof Pair<String>)//Error
同樣的道理猾担,getClass方法總是返回原始類型袭灯。例如:
Pair<String> stringPair = . . .;
Pair<Employee> employeePair = . . .;
if(stringPair.getClass() == employeePair.getClass()) //equal
其比較結(jié)果是true,因?yàn)閮纱握{(diào)用getClass結(jié)果都返回Pair.class
3.不能創(chuàng)建參數(shù)化類型的數(shù)組
不能實(shí)例化參數(shù)化類型的數(shù)組绑嘹,例如:
Pair<String>[] table = new Pair<String> [10];//Error
為什么編譯器不支持上面這樣的做法呢稽荧?繼續(xù)使用逆向思維,我們站在編譯器的角度來(lái)考慮這個(gè)問(wèn)題工腋。
我們先來(lái)看一下下面這個(gè)例子:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
對(duì)于上面這段代碼還是很好理解姨丈,字符串?dāng)?shù)組不能存放整型元素,而且這樣的錯(cuò)誤往往要等到代碼運(yùn)行的時(shí)候才能發(fā)現(xiàn)擅腰,編譯器是無(wú)法識(shí)別的构挤。接下來(lái)我們?cè)賮?lái)看一下假設(shè)Java支持泛型數(shù)組的創(chuàng)建會(huì)出現(xiàn)什么后果:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();
假設(shè)我們支持泛型數(shù)組的創(chuàng)建,由于運(yùn)行時(shí)期類型信息已經(jīng)被擦除惕鼓,JVM實(shí)際上根本就不知道new ArrayList<String>()和new ArrayList<Integer>()的區(qū)別筋现。類似這樣的錯(cuò)誤假如出現(xiàn)才實(shí)際的應(yīng)用場(chǎng)景中,將非常難以察覺(jué)箱歧。
如果你對(duì)上面這一點(diǎn)還抱有懷疑的話矾飞,可以嘗試運(yùn)行下面這段代碼:
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2); // true
}
}
最安全而有效的方法是使用ArrayList,即ArrayList<Pair<String>>
4.Varargs警告
考慮下面的方法呀邢,它有可變長(zhǎng)的泛型類型參數(shù)ts:
public static<T> void addAll(Collection<T> coll,T...ts){
for(t : ts) coll.add(t);
}
現(xiàn)在如果調(diào)用:
Collection<Pair<String>> table = . . .;
Pair<String> pair1 = . . .;
Pair<String> pair2 = . . .;
addAll(table,pair1,pair2);
JVM會(huì)為了可變長(zhǎng)參數(shù)建立一個(gè)Pair<String>數(shù)組洒沦,這違反了前面的規(guī)則。
這里特別注意:Java不支持泛型類型的數(shù)組价淌,但如果向參數(shù)個(gè)數(shù)可變的方法傳遞一個(gè)泛型類型的實(shí)例(即傳遞一個(gè)泛型類型對(duì)象的數(shù)組)這種規(guī)則有所放松申眼,你只會(huì)得到一個(gè)警告,而不是錯(cuò)誤蝉衣±ㄊ可以用@SuppressWarnings("unchecked")
或用@SafeVarags
標(biāo)注addAll方法來(lái)抑制警告。
即如下:
@SafeVarargs
public static<T> void addAll(Collection<T> coll,T...ts)
現(xiàn)在就可以提供泛型類型來(lái)調(diào)用這個(gè)方法了病毡。
5.不能實(shí)例化類型變量
不能使用像new T(...),new T[...]或T.class這樣的表達(dá)式中的類型變量濒翻。例如,下面的Pair<T>
構(gòu)造器是非法的:
public Pair() {first = new T();second = new T();}//Error
類型擦除將T改變成Object啦膜,而且本意肯定不希望調(diào)用new Object()有送。在Java SE 8之后,最好的解決方法是讓調(diào)用者提供一個(gè)構(gòu)造器表達(dá)式僧家。例如:
Pair<String> p = Pair.makePair(String::new);
makePair方法接受一個(gè)Supplier<T>
雀摘,這是一個(gè)函數(shù)式接口,表示一個(gè)無(wú)參數(shù)而且返回類型為T的函數(shù):
public static <T> Pair<T> makePair(Supplier<T> constr)
{
return new Pair<> (constr.get(),constr.get());
}
比較傳統(tǒng)的解決方法是通過(guò)反射調(diào)用Class.newInstance方法來(lái)構(gòu)造泛型對(duì)象八拱。
遺憾的是阵赠,細(xì)節(jié)有點(diǎn)復(fù)雜烁落。不能調(diào)用:
first = T.class.newInstance();//Error
表達(dá)式T.class是不合法的,因?yàn)樗鼤?huì)被類型擦除為Object.class豌注。必須像下面這樣設(shè)計(jì)API以便得到一個(gè)Class對(duì)象:
public static <T> Pair<T> makePair(Class<T> c1)
{
try {return new Pair<> (c1.newInstance(),c1.newInstance())};
catch(Exception ex) {return null;}
}
這個(gè)方法可以按照下列方式調(diào)用:
Pair<String> p = Pair.makePair(String.class);
注意伤塌,Class類本身是泛型。例如,String.class是一個(gè)Class<String>
的實(shí)例(事實(shí)上轧铁,它是唯一的實(shí)例)每聪。因此,makePair方法能夠推斷除pair的類型。
6.不能構(gòu)造泛型數(shù)組
就像不能實(shí)例化一個(gè)泛型實(shí)例一樣齿风,也不能實(shí)例化數(shù)組药薯。不過(guò)原因有所不同,畢竟數(shù)組會(huì)填充null值救斑,構(gòu)造時(shí)看上去是安全的童本。不過(guò),數(shù)組本身也有類型脸候,用來(lái)監(jiān)控存儲(chǔ)在虛擬機(jī)中的數(shù)組穷娱。這個(gè)類型會(huì)被擦除。例如运沦,考慮下面的例子:
public static <T extends Comparable> T[] minmax(T[] a) {T[] mm = new T[2];}//Error
類型擦除會(huì)讓這個(gè)方法永遠(yuǎn)構(gòu)造Comparable[2]數(shù)組泵额。
如果數(shù)組僅僅作為一個(gè)類的私有實(shí)例域,就可以將這個(gè)數(shù)組聲明為Object[],并且在屈原素時(shí)進(jìn)行類型轉(zhuǎn)換携添。例如嫁盲,ArrayList可以這樣實(shí)現(xiàn):
public class ArrayList<E>
{
private Object[] elements;
. . .
@SuppressWarnings("unchecked")
public E get(int i) { return (E) elements[i];}//取元素時(shí)強(qiáng)制類型轉(zhuǎn)換
public void set(int i,E e) { elements[i] = e;}//改變?cè)貢r(shí)不必強(qiáng)轉(zhuǎn)
}
實(shí)際的實(shí)現(xiàn)沒(méi)有那么清晰:
public class ArrayList<E>
{
private E[] elements;
. . .
public ArrayList() {elements = (E[]) new Object[10];}
}
這里的強(qiáng)制類型轉(zhuǎn)換E[]是一個(gè)假象,而類型擦除使其無(wú)法察覺(jué)烈掠。
由于 minmax 方法返回 T[] 數(shù)組羞秤,使得這一技術(shù)無(wú)法施展, 如果掩蓋這個(gè)類型會(huì)有運(yùn)行時(shí)錯(cuò)誤結(jié)果左敌。假設(shè)實(shí)現(xiàn)代碼:
public static <T extends Comparable> T[] minmax(T... a)
{
Object[] mm = new Object[2];
. . .
return (T[]) mm; // compiles with warning
}
調(diào)用 String[] ss = ArrayAlg.minmax("Tom", "Dick", "Harry");
編譯時(shí)不會(huì)有任何警告瘾蛋。當(dāng) Object[] 引用賦給 Comparable[] 變量時(shí),將會(huì)發(fā)生 ClassCastException異常母谎。
在這種情況下瘦黑, 最好讓用戶提供一個(gè)數(shù)組構(gòu)造器表達(dá)式:
String[] ss = ArrayAlg.minmax (String[]::new京革,"Tom", "Dick", "Harry");
構(gòu)造器表達(dá)式 String::new 指示一個(gè)函數(shù)奇唤,給定所需的長(zhǎng)度,會(huì)構(gòu)造一個(gè)指定長(zhǎng)度的
String數(shù)組匹摇。
minmax方法使用這個(gè)參數(shù)生成一個(gè)有正確類型的數(shù)組:
public static <T extends Comparable〉T[] minmax(IntFunction<T[]> constr, T... a)
{
T[] mm = constr.apply(2);
}
比較老式的方法是利用反射咬扇, 調(diào)用 Array.newInstance:
public static <T extends Comparable〉T[] minmaxfT... a)
{
T[] mm = (T[]) Array.newlnstance (a.getClass().getComponentType() , 2);
. . .
}
ArrayList 類的 toArray 方法就沒(méi)有這么幸運(yùn)。它需要生成一個(gè) T[] 數(shù)組廊勃, 但沒(méi)有成分類型懈贺。因此经窖, 有下面兩種不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二個(gè)方法接收一個(gè)數(shù)組參數(shù)。如果數(shù)組足夠大,就使用這個(gè)數(shù)組梭灿。否則,用 result 的成分類型構(gòu)造一個(gè)足夠大的新數(shù)組画侣。
7.泛型類的靜態(tài)上下文中類型變量無(wú)效
不能在靜態(tài)域或方法中引用類型變量。例如堡妒, 下列高招將無(wú)法施展:
public class Singleton<T>
{
private static T singlelnstance; // Error
public static T getSinglelnstanceO // Error
{
if (singleinstance == null) //construct new instance of T
return singlelnstance;
}
}
8.不能拋出或捕獲泛型類的實(shí)例
既不能拋出也不能捕獲泛型類對(duì)象配乱。實(shí)際上, 甚至泛型類擴(kuò)展 Throwable 都是不合法的皮迟。
例如搬泥, 以下定義就不能正常編譯:
public class Problem<T> extends Exception { /* . . . */ }
// Error can't extend Throwable
catch 子句中不能使用類型變量。例如伏尼, 以下方法將不能編譯:
public static <T extends Throwable〉void doWork(Class<T> t)
{
try
{
do work
}
catch (T e) // Error can 't catch type variable
{
Logger.global.info(...)
}
}
不過(guò)忿檩,在異常規(guī)范中使用類型變量是允許的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T // OK
{
try
{
do work
}
catch (Throwable real Cause)
{
t.initCause(real Cause);
throw t;
}
}
9.可以消除對(duì)受查異常的檢查
Java 異常處理的一個(gè)基本原則是爆阶, 必須為所有受查異常提供一個(gè)處理器燥透。不過(guò)可以利用泛型消除這個(gè)限制。關(guān)鍵在于以下方法:
@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T
{
throw (T) e;
}
假設(shè)這個(gè)方法包含在類 Block 中辨图,如果調(diào)用Block.<RuntimeException>throwAs(t);
編譯器就會(huì)認(rèn)為 t 是一個(gè)非受查異常兽掰。 以下代碼會(huì)把所有異常都轉(zhuǎn)換為編譯器所認(rèn)為的非受查異常:
try
{
do work
}
catch (Throwable t)
{
Block.<RuntimeException>throwAs(t) ;
}
下面把這個(gè)代碼包裝在一個(gè)抽象類中。用戶可以覆蓋 body 方法來(lái)提供一個(gè)具體的動(dòng)作徒役。調(diào)用 toThread 時(shí)孽尽, 會(huì)得到 Thread 類的一個(gè)對(duì)象, 它的 run 方法不會(huì)介意受查異常忧勿。
public abstract class Block
{
public abstract void body() throws Exception;
public Thread toThrea()
{
return new Thread()
{
public void run()
{
try
{
body();
}
catch (Throwable t)
{
Block.<RuntimeException> throwAs(t);
}
}
};
}
@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T
{
throw (T) e;
}
例如杉女, 以下程序運(yùn)行了一個(gè)線程, 它會(huì)拋出一個(gè)受查異常鸳吸。
public class Test
{
public static void main(String[] args)
{
new Block()
{
public void body() throws Exception
{
Scanner in = new Scanner(new File("ququx") ,"UTF-8");
while (in.hasNext())
System.out.println(in.next());
}
}
.toThread() .start();
}
}
運(yùn)行這個(gè)程序時(shí)熏挎, 會(huì)得到一個(gè)棧軌跡, 其中包含一個(gè)FileNotFoundException ( 當(dāng)然,假設(shè)你沒(méi)有提供一個(gè)名為 ququx 的文件)晌砾。
這有什么意義呢坎拐? 正常情況下, 你必須捕獲線程 run 方法中的所有受查異常养匈, 把它們"包裝"到非受查異常中哼勇, 因?yàn)?run 方法聲明為不拋出任何受查異常。
不過(guò)在這里并沒(méi)有做這種"包裝"呕乎。我們只是拋出異常积担, 并"哄騙"編譯器, 讓它認(rèn)為這不是一個(gè)受查異常猬仁。
通過(guò)使用泛型類帝璧、 擦除和 @SuppressWarnings 注解先誉, 就能消除 Java 類型系統(tǒng)的部分基本限制。
注意擦除后的沖突
當(dāng)泛型類型被擦除時(shí)的烁,無(wú)法創(chuàng)建引發(fā)沖突的條件褐耳。下面是一個(gè)示例。假定像下面這樣將equals 方法添加到 Pair 類中:
public class Pair<T>
{
public boolean equals(T value) { return first.equals(value) && second.equals(value); }
}
考慮一個(gè)Pair<String>
渴庆。從概念上講漱病,它有兩個(gè)equals方法:
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object
但是,直覺(jué)把我們引入歧途把曼。方法擦除boolean equals(T)
杨帽,就是boolean equals(Object)
與 Object.equals 方法發(fā)生沖突。
當(dāng)然嗤军,補(bǔ)救的辦法是重新命名引發(fā)錯(cuò)誤的方法注盈。
泛型規(guī)范說(shuō)明還提到另外一個(gè)原則:"要想支持擦除的轉(zhuǎn)換, 就需要強(qiáng)行限制一個(gè)類或類型變量不能同時(shí)成為兩個(gè)接口類型的子類叙赚,而這兩個(gè)接口是同一接口的不同參數(shù)化老客。"例如,
下述代碼是非法的:
class Employee implements Coinparab1e<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Hanager>
{ . . . } // Error
Manager 會(huì)實(shí)現(xiàn) Comparable<Employee>
和 Comparable<Manager>
, 這是同一接口的不同參數(shù)化震叮。
這一限制與類型擦除的關(guān)系并不十分明確胧砰。畢竟,下列非泛型版本是合法的苇瓣。
class Employee implements Comparable { . . . }
class Manager extends Employee implements Comparable { . . . }
其原因非常微妙尉间, 有可能與合成的橋方法產(chǎn)生沖突。實(shí)現(xiàn)了 Comparable<X>
的類可以獲得一個(gè)橋方法:
public int compareTo(Object other) { return compareTo((X) other); }
對(duì)于不同類型的 X 不能有兩個(gè)這樣的方法击罪。