什么是泛型
泛型程序設(shè)計就是為了讓一段代碼能夠被很多不同類型的對象所重用。Java提供的ArrayList
就使用了泛型刊懈,使得該類能夠存儲多種不同的類型對象把将,比如ArrayList<String>
保存String
對象夷恍、ArrayList<Integer>
保存Integer
對象童擎。這里<String>
、<Integer>
中被尖括號包起來的就是類型參數(shù)苏章,類型參數(shù)表明了該泛型的實例對象中元素的類型寂嘉。
使用泛型
我們可以定義泛型類和泛型方法奏瞬,下面是兩個例子。
泛型類
泛型類就是具有一個或多個泛型變量的類泉孩。下面是一個泛型類的例子硼端。例子中T
即為泛型變量,泛型變量使用的是大寫形式寓搬,一般有E
表示集合的元素類型珍昨,K
和V
表示鍵值對的類型、T
句喷、U
镣典、S
表示任意類型。使用了泛型變量后唾琼,就可以在類定義中使用T
來代表方法的參數(shù)兄春、方法的返回類型和變量的類型。
public class Pair<T>{
private T first;
private T second;
public void setFirst(T first){...}
public T getFirst(){...}
}
泛型方法
我們可以不定義泛型類锡溯,而在一個具體的類定義中使用泛型方法赶舆。下面是一個泛型方法的例子。泛型方法的泛型變量放置在修飾符的后面祭饭,返回類型的前面芜茵。在調(diào)用泛型方法的時候可以顯示的說明具體類型,也可以在有足夠信息讓編譯器推斷出類型的時候省略類型參數(shù)(當(dāng)然倡蝙,如果信息不夠就會拋出異常)夕晓。
class ArrayAlg{
public static <T> T getMiddle(T... a){}
}
String middle=ArrayAlg.<String>getMiddle("A","B","C");
泛型變量
在定義泛型類或泛型方法的時候,可以使用的泛型變量不僅僅上上面所提到的T
悠咱、E
等,還可以通過extends
關(guān)鍵詞來限定征炼,比如使用<T extends Comparable>
來表明T
為Comparable
這個限定類型的子類型析既,或者可以粗略的理解為其派生類,不過這個例子里并不是說子類型必須是限定類型的派生類谆奥,也可以是實現(xiàn)了限定類型接口的子類型眼坏。
當(dāng)限定類型有多個的時候,可以使用&
來進行連接酸些,比如<T extends Comparable & Seriablizable>
宰译。
虛擬機中的泛型代碼
虛擬機沒有泛型類型對象,所有對象都屬于普通類魄懂,因此泛型在編譯后會被替換為原始類型沿侈。
類型擦除
泛型信息只存在于代碼編譯階段,在進入 JVM 之前市栗,與泛型相關(guān)的信息會被擦除掉缀拭,專業(yè)術(shù)語叫做類型擦除咳短。在定義一個泛型類型的時候,都自動提供了一個相應(yīng)的原始類型蛛淋,比如在泛型類和泛型方法的例子中原始類型為Object
咙好,在使用了限定類型的時候原始類型就為限定類型
。這個過程就是擦除
類型變量褐荷,將其替換為限定類型
勾效。對于上面的ArrayAlg
被擦除后的原始類型如下。
public class ArrayAlg{
public statac Object getMiddle(Object... a){}
}
在類型擦除后叛甫,在調(diào)用的時候?qū)l(fā)生類型轉(zhuǎn)換层宫。比如String middle=ArrayAlg.<String>getMiddle("A","B","C");
,在實際執(zhí)行的時候會先調(diào)用原始類型的getMiddle()
然后再將Object
轉(zhuǎn)換為String
類型返回合溺。
在對泛型進行類型擦除后卒密,可能會出現(xiàn)兩個復(fù)雜的問題。
-
兩個Setter
上述的Pair<T>
被擦除后棠赛,其setter
方法將會變?yōu)?code>public void setFirst(Object first)哮奇。當(dāng)有一個類繼承了Pair<LocalDate>
類并重寫了setFirst()
的時候,將會出現(xiàn)兩個setFirst()
方法(參數(shù)類型不同)睛约。
public class DateInterval extends Pair<LocalData>{
public void setFirst(LocalData first){...}
}
//被擦除后的結(jié)果
public class DateInterval extends Pair{
//派生類中實現(xiàn)的
public void setFirst(LocalData first){...}
//Pair中繼承的
public void setFirst(Object first){...}
}
該類被擦除后的代碼如上鼎俘,此時Pair
中原來的setFirst(T first)
就變成了setFirst(Object first)
,與DateInterval
中定義的setter
方法參數(shù)不同辩涝,因此DateInterval
中就出現(xiàn)了兩個setFirst
方法贸伐。
為了解決這個問題,我們可以讓編譯器自行決定使用最適合的方法怔揩,或者使用一個橋方法重寫繼承的setter
捉邢。
public void setFirst(Object first){
setFirst((LocalData) first);
}
-
兩個getter
兩個getter
方法與上面的問題相似,當(dāng)派生類重寫了getFirst()
就會出現(xiàn)兩個getter
方法(返回類型不同)商膊,一個返回Object
伏伐,一個返回LocalData
,這個在Java編碼中是不允許的晕拆。這個問題我們不用過多操心藐翎,虛擬機能夠自行處理這個問題。
調(diào)用遺留代碼
假設(shè)我們實現(xiàn)了一個JSlider
類实幕,通過setLabelTable(Dictionary table)
來設(shè)置JSlider
標簽吝镣,這里Dictionary
是原始類型。如果我們通過Dictionary<Integer,Component>
實例化了一個泛型類Dictionary
昆庇,再調(diào)用setter
方法末贾,編譯器將會進行警告,因為編譯器無法確定setter
方法會對Dictionary
對象做什么操作凰锡,可能會將Integer
進行替換未舟,使得關(guān)鍵字不再是Integer
圈暗。這種情況下,我們可以通過@SuppressWarnings("unchecked")
進行標注裕膀,關(guān)閉對方法中代碼的檢查员串。
泛型的約束與限制
在使用泛型的時候,有很多限制需要注意昼扛,其中大多數(shù)都是由類型擦除引起的寸齐。
不能使用基本類型實例化類型參數(shù)
沒有Pair<int>
,只有Pair<Integer>
抄谐,這是因為Pair
類含有Object
類型的域渺鹦,而Object
不能存儲int
值。
運行時類型查詢只適用于原始類型
在虛擬機中泛型被擦除為原始類型蛹含,因此類型查詢只返回原始類型毅厚。
-
if(a instanceof Pair<String>)//error
實際上只測試a
是否為任意類型的一個Pair
。 -
getClass
也總是返回原始類型浦箱,if(stringPair.getClass()==employeePair.getClass())// true
吸耿。 - 在強制類型轉(zhuǎn)換時,也會拋出錯誤酷窥,
Pair<String> p=(Pair<String>)a;//Warning-can only test that a is a Pair
咽安。
不能創(chuàng)建參數(shù)化類型的數(shù)組
不能實例化參數(shù)化類型的數(shù)組,例如Pair<String>[] table=new Pair<String>[10];//error
蓬推。在這種情況下妆棒,擦除之后table
的類型實際為Pair[]
,程序員可以將其轉(zhuǎn)換為Object[]
沸伏,即Object[] objArray= table;
糕珊。這樣轉(zhuǎn)換后,如果試圖存儲除了Pair
的其他類型如objArray[0]="string";
毅糟,將會得到錯誤Error-component type is Pair
放接。因此,無法差un關(guān)鍵參數(shù)化類型的數(shù)組留特。
我們可以聲明通配類型的數(shù)組,然后進行類型轉(zhuǎn)換來通過編譯玛瘸,Pair<String>[] table=(Pair<String>[])new Pair<?>[10];
蜕青。這種方法是不安全的,如果往table
中存儲Pair<Employee>
是可以成功的糊渊,但是對table[0].getFirst()
調(diào)用一個String
將會報出錯誤右核。
Varargs警告
在調(diào)用一個參數(shù)個數(shù)可變的函數(shù)時,如public static <T> void addAll(Collection<T> coll,T... ts)
渺绒,ts
實際上是一個數(shù)組贺喝,包含所有的實參菱鸥。上面我們提到過,無法創(chuàng)建參數(shù)化類型的數(shù)組躏鱼,在這里Java虛擬機必須創(chuàng)建一個Pair<T>
數(shù)組氮采。對于這種情況,我們在調(diào)用的時候只會得到一個警告染苛,我們可以通過@SupressWarning("unchecked")
或@SafeVarargs
標注關(guān)閉方法中代碼的檢測鹊漠。
不能實例化類型變量
在泛型類中或泛型方法中,不能使用像new T(...)
茶行、T.class
這樣的表達式來實例化類型變量躯概,因為類型變量被擦除后會變成Object
對象,而本意是不希望掉用new Object()
的畔师。下面這個例子就是非法的娶靡。
public class Pair<T>{
public Pair(){
first=new T();
}
}
解決辦法是讓調(diào)用者提供一個構(gòu)造器的表達方式。
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ù)組的原因是類型擦除會使得永遠創(chuàng)建相同類型的數(shù)組看锉。比如T[] array=new T[2]
姿锭,實際上會永遠創(chuàng)建原始類型的數(shù)組。
如果創(chuàng)建該數(shù)組只是為了作為一個私有變量操作度陆,那么可以創(chuàng)建Object
數(shù)組艾凯,然后在調(diào)用的時候再進行類型轉(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];
}
}
這種方法不是萬能的懂傀,當(dāng)限定類型不能通過Object
強制轉(zhuǎn)換得到時趾诗,將會發(fā)生ClassCastException
異常。這時候的解決辦法就是提供一個數(shù)組構(gòu)造器表達式蹬蚁。
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)上下文中類型變量無效
不能在靜態(tài)域或方法中引用類型變量恃泪。
public class Singletion<T>{
private static T singletonIns;//error
public static T getSingleIns(){//error
if(singletonIns==null) construct one;
return singletonIns;
}
}
不能拋出或捕獲泛型類的實例
不能拋出也不能捕獲泛型類對象,泛型類擴展Throwable
也是不合法的犀斋。
public static <T extends Throwable> void doWork(Class<T> t){
try{}
catch(T e){}//error-cant catch type variable
}
不過贝乎,在異常規(guī)范中使用類型變量是允許的。(叽粹?)
public static <T extends Throwable> void doWork(T t) throws T{
try{}
catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
可以消除對受查異常的檢查
注意擦除后的沖突
當(dāng)泛型類型擦除時览效,可能會導(dǎo)致一些沖突。
- 方法沖突
在下面這個例子中虫几,Pair<T>
類中實現(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ī)范說明中有提到:“想要支持擦除的轉(zhuǎn)換但校,就需要強行限制一個類或類型變量不能同成為兩個接口類型的子類,而這兩個接口是同一接口的不同參數(shù)化啡氢。”例如下面的代碼就是非法的状囱。
class Employee implements Comparable<Employee>{}
class Manager extends Employee implements Comparable<Manager>{}//error
泛型類型的繼承規(guī)則
考慮一個類和其子類Employee
和Manager
术裸,Pair<Employee>
卻與Pair<Manager>
沒有任何關(guān)系,其之間沒有繼承的關(guān)系亭枷。也就是說無論S
和T
有什么聯(lián)系袭艺,Pair<S>
和Pair<T>
都沒有什么聯(lián)系。
通配符類型
上面說明了無論S
和T
有什么聯(lián)系奶栖,Pair<S>
和Pair<T>
都沒有什么聯(lián)系匹表。但是我們在實際使用的時候,并不希望Pair<Employee>
不能夠存儲Pair<Manager>
宣鄙,否則要分類存儲袍镀,變向地增加了代碼的繁瑣程度。Java中提供了通配符來解決這個問題冻晤。
通配符概念
首先要分清楚通配符與類型變量的區(qū)別苇羡。類型變量用于在定義泛型類或泛型方法的時候使用,而通配符是在使用泛型的時候來對泛型起限制作用鼻弧。
使用了通配符后设江,允許泛型實例的變量類型發(fā)生變化,比如Pair<? extends Employee>
表示任何泛型Pair
類型攘轩,它的類型參數(shù)是Employee
的子類叉存,不再僅僅局限于Employee
。這樣這個Pair
就既可以存儲Manager
又可以存儲Employee
了度帮。
但是這里存在一個細節(jié)問題歼捏,對于下面這個例子,setter
方法會出現(xiàn)編譯錯誤笨篷,而getter
方法不會瞳秽。這是因為setter
方法只說明了參數(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ù)外冕臭,還可以通過<? supper Employee>
表示Employee
的超類類型參數(shù)腺晾。
同樣,使用了超類類型限定后辜贵,無法使用getter
方法而可以使用setter
方法丘喻。
? supperEmployee getFirst();//error
void setFirst(? supperEmployee);
無限定通配符
可以使用無限定的通配符,Pair<?>
來表示存儲任何類型念颈。這個和原始類型的Pair
類型有較大的區(qū)別,使用該通配符后無法再使用setter
方法连霉。
無限定通配符通常被用來進行簡單的操作榴芳,比如下面的判斷是否為空引用嗡靡。
public static boolean hasNulls(Pair<?> P){
return p.getFirst()==null || p.getSecond()==null;
}
通配符捕獲
對于一個交換元素的方法public static void swap(Pair<?> p)
,我們無法在方法中使用? t=p.getFirst()
來初始化變量窟感,也就是說無法使用?
來表示類型讨彼。在這種情況下,我們需要通過一個輔助方法來解決柿祈。
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)然哈误,這里也可以直接使用泛型來解決這個問題。但是在想要使用supper
關(guān)鍵字而不得不使用通配符的時候躏嚎,該方法就是必須了蜜自。
public static void maxminBonus(Manager[] a,Pair<? supper Manager> result){
minmaxBonus(a,result);
PairAlg.swap(result);//這里就需要一個swapHelper
}
反射和泛型
泛型由于會發(fā)生類型擦除,因此在使用反射的時候得不到太多信息卢佣,下面將介紹用反射能夠得到的信息重荠。
泛型Class類
Class
類是泛型的,String.class
實際上是一個Class<String>
類的對象虚茶。下面是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ù)進行類型匹配
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,IllegalAccessException{
return new Pair<>(c.newInsatance(),c.newInstance());
}
虛擬機中的泛型類型信息
虛擬機中被擦除的類仍能夠保持一些泛型祖先的信息。對于方法public static Comparable min(Comparable[] a)
嘹叫,這是一個泛型方法public static <T extends Comparable<? super T>> T min(T[] a)
的擦除婆殿。我們可以通過反射API來確定:
- 這個泛型方法有一個叫做T的類型參數(shù)
- 這個類型參數(shù)有一個子類型限定,其自身又是一個泛型類型
- 這個限定類型有一個通配符參數(shù)
- 這個通配符參數(shù)有一個超類型限定
- 這個泛型方法有一個泛型數(shù)組參數(shù)
java.lang.reflect
包中提供了Type
接口罩扇,包含下列子類型: - Class類婆芦,描述具體類型
- TypeVariable接口,描述類型變量(如
T extends Comparable<? super T>
) - WildcardType接口暮蹂,描述通配符(如
? super T
) - ParameterizedType接口寞缝,描述泛型類或接口類型(如
Comparable<? super T>
) - GenericArrayType接口,描述泛型數(shù)組(如
T[]
)