什么是泛型
泛型程序設(shè)計(jì)就是為了讓一段代碼能夠被很多不同類型的對(duì)象所重用。Java提供的ArrayList就使用了泛型,使得該類能夠存儲(chǔ)多種不同的類型對(duì)象,比如ArrayList<String>保存String對(duì)象、ArrayList<Integer>保存Integer對(duì)象。這里<String>、<Integer>中被尖括號(hào)包起來(lái)的就是類型參數(shù),類型參數(shù)表明了該泛型的實(shí)例對(duì)象中元素的類型。
使用泛型
我們可以定義泛型類和泛型方法,下面是兩個(gè)例子。
泛型類
泛型類就是具有一個(gè)或多個(gè)泛型變量的類。下面是一個(gè)泛型類的例子。例子中T即為泛型變量,泛型變量使用的是大寫(xiě)形式,一般有E表示集合的元素類型,K和V表示鍵值對(duì)的類型、T、U、S表示任意類型。使用了泛型變量后,就可以在類定義中使用T來(lái)代表方法的參數(shù)、方法的返回類型和變量的類型。
public class Pair<T>{
private T first;
private T second;
public void setFirst(T first){...}
public T getFirst(){...}
}
泛型方法
我們可以不定義泛型類,而在一個(gè)具體的類定義中使用泛型方法。下面是一個(gè)泛型方法的例子。泛型方法的泛型變量放置在修飾符的后面,返回類型的前面。在調(diào)用泛型方法的時(shí)候可以顯示的說(shuō)明具體類型,也可以在有足夠信息讓編譯器推斷出類型的時(shí)候省略類型參數(shù)(當(dāng)然,如果信息不夠就會(huì)拋出異常)。
class ArrayAlg{
public static <T> T getMiddle(T... a){}
}
String middle=ArrayAlg.<String>getMiddle("A","B","C");
泛型變量
在定義泛型類或泛型方法的時(shí)候,可以使用的泛型變量不僅僅上上面所提到的T、E等,還可以通過(guò)extends關(guān)鍵詞來(lái)限定,比如使用<T extends Comparable>來(lái)表明T為Comparable這個(gè)限定類型的子類型,或者可以粗略的理解為其派生類,不過(guò)這個(gè)例子里并不是說(shuō)子類型必須是限定類型的派生類,也可以是實(shí)現(xiàn)了限定類型接口的子類型。
當(dāng)限定類型有多個(gè)的時(shí)候,可以使用&來(lái)進(jìn)行連接,比如<T extends Comparable & Seriablizable>。
虛擬機(jī)中的泛型代碼
虛擬機(jī)沒(méi)有泛型類型對(duì)象,所有對(duì)象都屬于普通類,因此泛型在編譯后會(huì)被替換為原始類型。
類型擦除
泛型信息只存在于代碼編譯階段,在進(jìn)入 JVM 之前,與泛型相關(guān)的信息會(huì)被擦除掉,專業(yè)術(shù)語(yǔ)叫做類型擦除。在定義一個(gè)泛型類型的時(shí)候,都自動(dòng)提供了一個(gè)相應(yīng)的原始類型,比如在泛型類和泛型方法的例子中原始類型為Object,在使用了限定類型的時(shí)候原始類型就為限定類型。這個(gè)過(guò)程就是擦除類型變量,將其替換為限定類型。對(duì)于上面的ArrayAlg被擦除后的原始類型如下。
public class ArrayAlg{
public statac Object getMiddle(Object... a){}
}
在類型擦除后,在調(diào)用的時(shí)候?qū)?huì)發(fā)生類型轉(zhuǎn)換。比如String middle=ArrayAlg.<String>getMiddle("A","B","C");,在實(shí)際執(zhí)行的時(shí)候會(huì)先調(diào)用原始類型的getMiddle()然后再將Object轉(zhuǎn)換為String類型返回。
在對(duì)泛型進(jìn)行類型擦除后,可能會(huì)出現(xiàn)兩個(gè)復(fù)雜的問(wèn)題。
-
兩個(gè)Setter
上述的Pair<T>被擦除后,其setter方法將會(huì)變?yōu)?code>public void setFirst(Object first)。當(dāng)有一個(gè)類繼承了Pair<LocalDate>類并重寫(xiě)了setFirst()的時(shí)候,將會(huì)出現(xiàn)兩個(gè)setFirst()方法(參數(shù)類型不同)。
public class DateInterval extends Pair<LocalData>{
public void setFirst(LocalData first){...}
}
//被擦除后的結(jié)果
public class DateInterval extends Pair{
//派生類中實(shí)現(xiàn)的
public void setFirst(LocalData first){...}
//Pair中繼承的
public void setFirst(Object first){...}
}
該類被擦除后的代碼如上,此時(shí)Pair中原來(lái)的setFirst(T first)就變成了setFirst(Object first),與DateInterval中定義的setter方法參數(shù)不同,因此DateInterval中就出現(xiàn)了兩個(gè)setFirst方法。
為了解決這個(gè)問(wèn)題,我們可以讓編譯器自行決定使用最適合的方法,或者使用一個(gè)橋方法重寫(xiě)繼承的setter。
public void setFirst(Object first){
setFirst((LocalData) first);
}
-
兩個(gè)getter
兩個(gè)getter方法與上面的問(wèn)題相似,當(dāng)派生類重寫(xiě)了getFirst()就會(huì)出現(xiàn)兩個(gè)getter方法(返回類型不同),一個(gè)返回Object,一個(gè)返回LocalData,這個(gè)在Java編碼中是不允許的。這個(gè)問(wèn)題我們不用過(guò)多操心,虛擬機(jī)能夠自行處理這個(gè)問(wèn)題。
調(diào)用遺留代碼
假設(shè)我們實(shí)現(xiàn)了一個(gè)JSlider類,通過(guò)setLabelTable(Dictionary table)來(lái)設(shè)置JSlider標(biāo)簽,這里Dictionary是原始類型。如果我們通過(guò)Dictionary<Integer,Component>實(shí)例化了一個(gè)泛型類Dictionary,再調(diào)用setter方法,編譯器將會(huì)進(jìn)行警告,因?yàn)榫幾g器無(wú)法確定setter方法會(huì)對(duì)Dictionary對(duì)象做什么操作,可能會(huì)將Integer進(jìn)行替換,使得關(guān)鍵字不再是Integer。這種情況下,我們可以通過(guò)@SuppressWarnings("unchecked")進(jìn)行標(biāo)注,關(guān)閉對(duì)方法中代碼的檢查。
泛型的約束與限制
在使用泛型的時(shí)候,有很多限制需要注意,其中大多數(shù)都是由類型擦除引起的。
不能使用基本類型實(shí)例化類型參數(shù)
沒(méi)有Pair<int>,只有Pair<Integer>,這是因?yàn)?code>Pair類含有Object類型的域,而Object不能存儲(chǔ)int值。
運(yùn)行時(shí)類型查詢只適用于原始類型
在虛擬機(jī)中泛型被擦除為原始類型,因此類型查詢只返回原始類型。
-
if(a instanceof Pair<String>)//error實(shí)際上只測(cè)試a是否為任意類型的一個(gè)Pair。 -
getClass也總是返回原始類型,if(stringPair.getClass()==employeePair.getClass())// true。 - 在強(qiáng)制類型轉(zhuǎn)換時(shí),也會(huì)拋出錯(cuò)誤,
Pair<String> p=(Pair<String>)a;//Warning-can only test that a is a Pair。
不能創(chuàng)建參數(shù)化類型的數(shù)組
不能實(shí)例化參數(shù)化類型的數(shù)組,例如Pair<String>[] table=new Pair<String>[10];//error。在這種情況下,擦除之后table的類型實(shí)際為Pair[],程序員可以將其轉(zhuǎn)換為Object[],即Object[] objArray= table;。這樣轉(zhuǎn)換后,如果試圖存儲(chǔ)除了Pair的其他類型如objArray[0]="string";,將會(huì)得到錯(cuò)誤Error-component type is Pair。因此,無(wú)法差un關(guān)鍵參數(shù)化類型的數(shù)組。
我們可以聲明通配類型的數(shù)組,然后進(jìn)行類型轉(zhuǎn)換來(lái)通過(guò)編譯,Pair<String>[] table=(Pair<String>[])new Pair<?>[10];。這種方法是不安全的,如果往table中存儲(chǔ)Pair<Employee>是可以成功的,但是對(duì)table[0].getFirst()調(diào)用一個(gè)String將會(huì)報(bào)出錯(cuò)誤。
Varargs警告
在調(diào)用一個(gè)參數(shù)個(gè)數(shù)可變的函數(shù)時(shí),如public static <T> void addAll(Collection<T> coll,T... ts),ts實(shí)際上是一個(gè)數(shù)組,包含所有的實(shí)參。上面我們提到過(guò),無(wú)法創(chuàng)建參數(shù)化類型的數(shù)組,在這里Java虛擬機(jī)必須創(chuàng)建一個(gè)Pair<T>數(shù)組。對(duì)于這種情況,我們?cè)谡{(diào)用的時(shí)候只會(huì)得到一個(gè)警告,我們可以通過(guò)@SupressWarning("unchecked")或@SafeVarargs標(biāo)注關(guān)閉方法中代碼的檢測(cè)。
不能實(shí)例化類型變量
在泛型類中或泛型方法中,不能使用像new T(...)、T.class這樣的表達(dá)式來(lái)實(shí)例化類型變量,因?yàn)轭愋妥兞勘徊脸髸?huì)變成Object對(duì)象,而本意是不希望掉用new Object()的。下面這個(gè)例子就是非法的。
public class Pair<T>{
public Pair(){
first=new T();
}
}
解決辦法是讓調(diào)用者提供一個(gè)構(gòu)造器的表達(dá)方式。
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(),constr.get());
}
Pair<String> p=Pair.makePair(String::new);
不能構(gòu)造泛型數(shù)組
在泛型類或方法中不能構(gòu)造泛型數(shù)組的原因是類型擦除會(huì)使得永遠(yuǎn)創(chuàng)建相同類型的數(shù)組。比如T[] array=new T[2],實(shí)際上會(huì)永遠(yuǎn)創(chuàng)建原始類型的數(shù)組。
如果創(chuàng)建該數(shù)組只是為了作為一個(gè)私有變量操作,那么可以創(chuàng)建Object數(shù)組,然后在調(diào)用的時(shí)候再進(jìn)行類型轉(zhuǎn)換。
public class ArrayList<E>{
private Object[] elements;
public ArrayList(){
elements=(E[]) new Object[10];
}
public E get(int n){
return (E) elements[n];
}
}
這種方法不是萬(wàn)能的,當(dāng)限定類型不能通過(guò)Object強(qiáng)制轉(zhuǎn)換得到時(shí),將會(huì)發(fā)生ClassCastException異常。這時(shí)候的解決辦法就是提供一個(gè)數(shù)組構(gòu)造器表達(dá)式。
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr,T... a){
T[] mm=constr.apply(2);
...
}
String[] ss=ArrayAlg.minmax(String[]::new,"A","B","C");
泛型類的靜態(tài)上下文中類型變量無(wú)效
不能在靜態(tài)域或方法中引用類型變量。
public class Singletion<T>{
private static T singletonIns;//error
public static T getSingleIns(){//error
if(singletonIns==null) construct one;
return singletonIns;
}
}
不能拋出或捕獲泛型類的實(shí)例
不能拋出也不能捕獲泛型類對(duì)象,泛型類擴(kuò)展Throwable也是不合法的。
public static <T extends Throwable> void doWork(Class<T> t){
try{}
catch(T e){}//error-cant catch type variable
}
不過(guò),在異常規(guī)范中使用類型變量是允許的。(?)
public static <T extends Throwable> void doWork(T t) throws T{
try{}
catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
可以消除對(duì)受查異常的檢查
注意擦除后的沖突
當(dāng)泛型類型擦除時(shí),可能會(huì)導(dǎo)致一些沖突。
- 方法沖突
在下面這個(gè)例子中,Pair<T>類中實(shí)現(xiàn)了euqals(T value)方法,在類型擦除后為euqals(Object value)方法,與從Object繼承的euqals(Object value)方法同名沖突。
public Pair<T>{
public boolean euqals(T value){
//Name clash:The method equals(T) of type Generics<T> has the same erasure as equals(Object) of type Object but does not override it
return first.equals(value)&&second.euqals(value);
}
}
- 類沖突
泛型規(guī)范說(shuō)明中有提到:“想要支持擦除的轉(zhuǎn)換,就需要強(qiáng)行限制一個(gè)類或類型變量不能同成為兩個(gè)接口類型的子類,而這兩個(gè)接口是同一接口的不同參數(shù)化。”例如下面的代碼就是非法的。
class Employee implements Comparable<Employee>{}
class Manager extends Employee implements Comparable<Manager>{}//error
泛型類型的繼承規(guī)則
考慮一個(gè)類和其子類Employee和Manager,Pair<Employee>卻與Pair<Manager>沒(méi)有任何關(guān)系,其之間沒(méi)有繼承的關(guān)系。也就是說(shuō)無(wú)論S和T有什么聯(lián)系,Pair<S>和Pair<T>都沒(méi)有什么聯(lián)系。

通配符類型
上面說(shuō)明了無(wú)論S和T有什么聯(lián)系,Pair<S>和Pair<T>都沒(méi)有什么聯(lián)系。但是我們?cè)趯?shí)際使用的時(shí)候,并不希望Pair<Employee>不能夠存儲(chǔ)Pair<Manager>,否則要分類存儲(chǔ),變向地增加了代碼的繁瑣程度。Java中提供了通配符來(lái)解決這個(gè)問(wèn)題。
通配符概念
首先要分清楚通配符與類型變量的區(qū)別。類型變量用于在定義泛型類或泛型方法的時(shí)候使用,而通配符是在使用泛型的時(shí)候來(lái)對(duì)泛型起限制作用。
使用了通配符后,允許泛型實(shí)例的變量類型發(fā)生變化,比如Pair<? extends Employee>表示任何泛型Pair類型,它的類型參數(shù)是Employee的子類,不再僅僅局限于Employee。這樣這個(gè)Pair就既可以存儲(chǔ)Manager又可以存儲(chǔ)Employee了。
但是這里存在一個(gè)細(xì)節(jié)問(wèn)題,對(duì)于下面這個(gè)例子,setter方法會(huì)出現(xiàn)編譯錯(cuò)誤,而getter方法不會(huì)。這是因?yàn)?code>setter方法只說(shuō)明了參數(shù)是Employee的派生類,但是不知道具體是什么類型,但是getter方法知道返回類型是Employee的派生類,可以將其派生類復(fù)制給Employee的引用。
Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies=managerBuddies;//ok
wildcardBuddies.setFirst(lowlyEmployee);//compile-time error
? extends Employee getFirst();
void setFirst(? extends Employee);//error
通配符超類型限定
除了上述的<? extends Employee>表示Employee的派生類類型參數(shù)外,還可以通過(guò)<? supper Employee>表示Employee的超類類型參數(shù)。
同樣,使用了超類類型限定后,無(wú)法使用getter方法而可以使用setter方法。
? supperEmployee getFirst();//error
void setFirst(? supperEmployee);
無(wú)限定通配符
可以使用無(wú)限定的通配符,Pair<?>來(lái)表示存儲(chǔ)任何類型。這個(gè)和原始類型的Pair類型有較大的區(qū)別,使用該通配符后無(wú)法再使用setter方法。
無(wú)限定通配符通常被用來(lái)進(jìn)行簡(jiǎn)單的操作,比如下面的判斷是否為空引用。
public static boolean hasNulls(Pair<?> P){
return p.getFirst()==null || p.getSecond()==null;
}
通配符捕獲
對(duì)于一個(gè)交換元素的方法public static void swap(Pair<?> p),我們無(wú)法在方法中使用? t=p.getFirst()來(lái)初始化變量,也就是說(shuō)無(wú)法使用?來(lái)表示類型。在這種情況下,我們需要通過(guò)一個(gè)輔助方法來(lái)解決。
public static <T> void swapHelper(Pair<T> p){
T t=p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
public static void swap(Pair<?> p){swapHelper(p);}
當(dāng)然,這里也可以直接使用泛型來(lái)解決這個(gè)問(wèn)題。但是在想要使用supper關(guān)鍵字而不得不使用通配符的時(shí)候,該方法就是必須了。
public static void maxminBonus(Manager[] a,Pair<? supper Manager> result){
minmaxBonus(a,result);
PairAlg.swap(result);//這里就需要一個(gè)swapHelper
}
反射和泛型
泛型由于會(huì)發(fā)生類型擦除,因此在使用反射的時(shí)候得不到太多信息,下面將介紹用反射能夠得到的信息。
泛型Class類
Class類是泛型的,String.class實(shí)際上是一個(gè)Class<String>類的對(duì)象。下面是Class<T>中使用了類型參數(shù)的方法。
T newInstance();
T cast(Object obj);
T[] getEnumConstants();
Class<? super T> getSuperclass();
Constructor<T> getConstructor(Class... parameterTypes);
Constructor<T> getDeclaredConstructor(Class... parameterTypes);
使用Class<T>參數(shù)進(jìn)行類型匹配
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,IllegalAccessException{
return new Pair<>(c.newInsatance(),c.newInstance());
}
虛擬機(jī)中的泛型類型信息
虛擬機(jī)中被擦除的類仍能夠保持一些泛型祖先的信息。對(duì)于方法public static Comparable min(Comparable[] a),這是一個(gè)泛型方法public static <T extends Comparable<? super T>> T min(T[] a)的擦除。我們可以通過(guò)反射API來(lái)確定:
- 這個(gè)泛型方法有一個(gè)叫做T的類型參數(shù)
- 這個(gè)類型參數(shù)有一個(gè)子類型限定,其自身又是一個(gè)泛型類型
- 這個(gè)限定類型有一個(gè)通配符參數(shù)
- 這個(gè)通配符參數(shù)有一個(gè)超類型限定
- 這個(gè)泛型方法有一個(gè)泛型數(shù)組參數(shù)
java.lang.reflect包中提供了Type接口,包含下列子類型: - Class類,描述具體類型
- TypeVariable接口,描述類型變量(如
T extends Comparable<? super T>) - WildcardType接口,描述通配符(如
? super T) - ParameterizedType接口,描述泛型類或接口類型(如
Comparable<? super T>) - GenericArrayType接口,描述泛型數(shù)組(如
T[])