一孕锄、概述
1.1 什么是Java泛型
Java 泛型(generics)是 JDK 5 中引入的一個(gè)新特性, 泛型提供了編譯時(shí)類型安全檢測(cè)機(jī)制吮廉,該機(jī)制允許程序員在編譯時(shí)檢測(cè)到非法的類型。泛型的本質(zhì)是參數(shù)化類型畸肆,也就是說(shuō)所操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)宦芦。這種參數(shù)類型可以用在類、接口和方法的創(chuàng)建中轴脐,分別稱為泛型類调卑、泛型接口、泛型方法大咱。
那么參數(shù)化類型怎么理解呢恬涧?就是將類型由原來(lái)的具體的類型參數(shù)化,類似于方法中的變量參數(shù)碴巾,此時(shí)類型也定義成參數(shù)形式(可以稱之為類型形參)溯捆,然后在使用/調(diào)用時(shí)傳入具體的類型(類型實(shí)參)。
1.2 為什么使用Java泛型
Java語(yǔ)言引入泛型的好處是安全簡(jiǎn)單厦瓢√嶙幔可以將運(yùn)行時(shí)錯(cuò)誤提前到編譯時(shí)錯(cuò)誤。
在Java SE 1.5之前煮仇,沒(méi)有泛型的情況的下劳跃,通過(guò)對(duì)類型Object的引用來(lái)實(shí)現(xiàn)參數(shù)的“任意化”,“任意化”帶來(lái)的缺點(diǎn)是要做顯式的強(qiáng)制類型轉(zhuǎn)換浙垫,而這種轉(zhuǎn)換是要求開發(fā)者對(duì)實(shí)際參數(shù)類型可以預(yù)知的情況下進(jìn)行的刨仑。對(duì)于強(qiáng)制類型轉(zhuǎn)換錯(cuò)誤的情況,編譯器可能不提示錯(cuò)誤夹姥,在運(yùn)行的時(shí)候才出現(xiàn)異常杉武,這是一個(gè)安全隱患。泛型的好處是在編譯的時(shí)候檢查類型安全佃声,并且所有的強(qiáng)制轉(zhuǎn)換都是自動(dòng)和隱式的艺智,提高代碼的重用率。
二圾亏、泛型的使用
2.1 泛型類
泛型類用于類的定義中十拣,被稱為泛型類封拧。通過(guò)泛型可以完成對(duì)一組類的操作對(duì)外開放相同的接口。泛型類的聲明和非泛型類的聲明類似夭问,除了在類名后面添加了類型參數(shù)聲明部分泽西。泛型類的類型參數(shù)聲明部分也包含一個(gè)或多個(gè)類型參數(shù),參數(shù)間用逗號(hào)隔開缰趋。最典型的就是各種容器類捧杉,如:List、Set秘血、Map味抖。
2.1.1 示例
//此處T可以隨便寫為任意標(biāo)識(shí)灰粮,常見的如T仔涩、E、K粘舟、V等形式的參數(shù)常用于表示泛型
public class CustomMap<T> {
//key這個(gè)成員變量的類型為T,T的類型由外部指定
private T key;
//泛型方法getKey的返回值類型為T熔脂,T的類型由外部指定
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
public static void main(String[] arg){
//泛型的類型參數(shù)只能是類類型(包括自定義類),不能是簡(jiǎn)單類型
CustomMap<String> a = new CustomMap <>();
//傳入的實(shí)參類型需與泛型的類型參數(shù)類型相同柑肴,即為Integer.
a.setKey("aas");
CustomMap<Integer> b = new CustomMap <>();
b.setKey(23);
System.out.println(a.getKey());
System.out.println(b.getKey());
}
在使用泛型的時(shí)候如果傳入泛型實(shí)參霞揉,則會(huì)根據(jù)傳入的泛型實(shí)參做相應(yīng)的限制,此時(shí)泛型才會(huì)起到本應(yīng)起到的限制作用晰骑。如果不傳入泛型類型實(shí)參的話适秩,在泛型類中使用泛型的方法或成員變量定義的類型可以為任何的類型。
public static void main(String[] arg){
CustomMap a = new CustomMap <>();
a.setKey("aas");
CustomMap b = new CustomMap <>();
b.setKey(23);
System.out.println(a.getKey());
System.out.println(b.getKey());
}
注意:
- 泛型的類型參數(shù)只能是類類型些侍,不能是簡(jiǎn)單類型
- 不能對(duì)確切的泛型類型使用instanceof操作隶症。如
if( a instanceof CustomMap<String>)
是非法的,編譯時(shí)會(huì)出錯(cuò)
2.2 泛型接口
泛型接口與泛型類的定義及使用基本是相同的岗宣。
2.2.1 示例
定義一個(gè)泛型接口
public interface ICustomMap<T> {
T getItem(T item);
}
實(shí)現(xiàn)泛型接口,傳入泛型實(shí)參
//在實(shí)現(xiàn)類實(shí)現(xiàn)泛型接口時(shí)淋样,如已將泛型類型傳入實(shí)參類型耗式,則所有使用泛型的地方都要替換成傳入的實(shí)參類型
public class CustomMap implements ICustomMap<String> {
@Override
public String getItem(String item) {
return item;
}
}
實(shí)現(xiàn)泛型接口,未傳入泛型實(shí)參
//未傳入泛型實(shí)參時(shí)趁猴,與泛型類的定義相同,在聲明類的時(shí)候儡司,需將泛型的聲明也一起加到類中
//如果不聲明泛型捕犬,如:class CustomMap implements CustomMap<T>,編譯器會(huì)報(bào)錯(cuò):"Unknown class"
public class CustomMap<T> implements ICustomMap<T> {
@Override
public T getItem(T item) {
return item;
}
}
2.3 泛型方法
泛型方法相對(duì)于泛型類淮韭,泛型接口比較復(fù)雜靠粪。泛型方法在調(diào)用時(shí)可以接收不同類型的參數(shù)占键。根據(jù)傳遞給泛型方法的參數(shù)類型捞慌,編譯器適當(dāng)?shù)靥幚砻恳粋€(gè)方法調(diào)用啸澡。
/**
* 泛型方法
* @param tClass 傳入的泛型實(shí)參
* @return T 返回值為T類型
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
說(shuō)明:
- public 與返回值中間 <T> 非常重要嗅虏,可以理解為聲明此方法為泛型方法
- 只有聲明了 <T> 的方法才是泛型方法皮服,泛型類中的使用了泛型的成員方法并不是泛型方法
- <T> 表明該方法將使用泛型類型T龄广,此時(shí)才可以在方法中使用泛型類型T
- 與泛型類的定義一樣择同,T可以隨便寫為任意標(biāo)識(shí)敲才,常見的如T紧武、E、K已添、V等形式的參數(shù)常用于表示泛型
2.3.1 簡(jiǎn)單示例
class test {
//這是一個(gè)泛型類
public class Custom<T>{
private T key;
public T getKey() {
return key;
}
//雖然在方法中使用了泛型,但是這并不是一個(gè)泛型方法恨狈。
public void setKey(T key) {
this.key = key;
}
}
/**
* 這才是一個(gè)真正的泛型方法
*
* 首先在public與返回值之間的<T>必不可少禾怠,這表明這是一個(gè)泛型方法吗氏,并且聲明了一個(gè)泛型T
* 這個(gè)T可以出現(xiàn)在這個(gè)泛型方法的任意位置.
* 泛型的數(shù)量也可以為任意多個(gè)
* 如:public <K,V,T> T showValue1(Map<K,V> map,List<T> container){
* ...
* }
*/
public <T> T showValue1(List<T> list){
return list.get(0);
}
//這不是一個(gè)泛型方法,只是使用了List<String>這個(gè)泛型類做形參而已
public void showValue2(List<String> list){
}
//這不是一個(gè)泛型方法,只不過(guò)使用了泛型通配符?
//?是一種類型實(shí)參往产,可以看做為String等所有類的父類
public void showValue3(List<?> list){
}
}
2.3.2 泛型類中使用泛型方法
class test {
public class Custom<T>{
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
//在泛型類中聲明了一個(gè)泛型方法仿村,使用泛型E蔼囊,這種泛型E可以為任意類型畏鼓。可以類型與T相同佃迄,也可以不同
//由于泛型方法在聲明的時(shí)候會(huì)聲明泛型<E>呵俏,因此即使在泛型類中并未聲明泛型,編譯器也能夠正確識(shí)別泛型方法中識(shí)別的泛型麻车。
public <E> void showValue1(E t){
System.out.println(t.toString());
}
//在泛型類中聲明了一個(gè)泛型方法,使用泛型T啤斗,注意這個(gè)T是一種全新的類型钮莲,可以與泛型類中聲明的T不是同一種類型。
public <T> void showValue2(T t){
System.out.println(t.toString());
}
}
}
2.3.3 泛型方法的可變參數(shù)
public <T> void showValue( T... args){
for(T t : args){
System.out.println(t.toString());
}
}
2.3.4 靜態(tài)方法中使用泛型
靜態(tài)方法無(wú)法訪問(wèn)類上定義的泛型链瓦;如果靜態(tài)方法操作的引用數(shù)據(jù)類型不確定的時(shí)候慈俯,必須要將泛型定義在方法上贴膘。即:如果靜態(tài)方法要使用泛型的話,必須將靜態(tài)方法也定義成泛型方法
public class CustomMap<T> {
/**
* 如果在類中定義使用泛型的靜態(tài)方法氛琢,需要添加額外的泛型聲明(將這個(gè)方法定義成泛型方法)
* 即使靜態(tài)方法要使用泛型類中已經(jīng)聲明過(guò)的泛型也不可以阳似。
* 如:public static void show(T t){..},此時(shí)編譯器會(huì)提示錯(cuò)誤信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void showValue(T t){
}
}
2.4 泛型通配符
通配符有三種使用方式:
- 上限通配符:
? extends
擴(kuò)展類型 - 下限通配符:
? super
超級(jí)類型 - 無(wú)限通配符:
?
2.4.1 上限通配符
問(wèn)號(hào)(?)表示通配符撮奏,代表未知類型的泛型。有時(shí)候可能希望限制允許傳遞給類型參數(shù)的類型玲献。 例如捌年,對(duì)數(shù)字進(jìn)行操作的方法可能只希望接受Number類或其子類的實(shí)例礼预。要聲明一個(gè)上限通配符參數(shù)褒颈,首先列出問(wèn)號(hào) ? 然后跟上extend關(guān)鍵字吵冒,之后再指定其上限悉默。
示例
class test {
public static double sum(List <? extends Number> numberlist) {
double sum = 0.0;
for (Number n : numberlist)
sum += n.doubleValue();
return sum;
}
public static void main(String args[]) {
List<Integer> integerList = Arrays.asList(10, 12, 23);
System.out.println("sum = " + sum(integerList));
List<Double> doubleList = Arrays.asList(11.2, 32.3, 53.7);
System.out.println("sum = " + sum(doubleList));
}
}
2.4.2 無(wú)界通配符
問(wèn)號(hào)(?)表示通配符菱蔬,代表未知類型的泛型漩绵。 當(dāng)可以使用Object類中提供的功能或當(dāng)代碼獨(dú)立于類型參數(shù)來(lái)實(shí)現(xiàn)方法時(shí),這樣的參數(shù)可以使用任何對(duì)象碍扔。要聲明無(wú)限通配符參數(shù)不同,只需要列出問(wèn)號(hào)(?)二拐。
示例
class test {
public static void printAll(List<?> list) {
for (Object item : list)
System.out.println(item + " ");
}
public static void main(String args[]) {
List<Integer> integerList = Arrays.asList(10, 20, 30);
printAll(integerList);
List<Double> doubleList = Arrays.asList(11.2, 12.3, 13.5);
printAll(doubleList);
}
}
2.4.3 下限通配符
問(wèn)號(hào)(?)表示通配符百新,代表未知類型的泛型吟孙。 有時(shí)候可能希望限制允許傳遞給類型參數(shù)的類型杰妓。 例如巷挥,對(duì)數(shù)字進(jìn)行操作的方法可能只需要接受Integer或其超類的實(shí)例倍宾,例如Number類的實(shí)例。要聲明一個(gè)下限通配符參數(shù)怔锌,在問(wèn)號(hào)?后跟super關(guān)鍵字变过,最后跟其下界媚狰。
示例
class test {
public static void addCat(List<? super Cat> catList) {
catList.add(new RedCat());
System.out.println("Cat Added");
}
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<Animal>();
List<Cat> catList = new ArrayList<Cat>();
List<RedCat> redCatList = new ArrayList<RedCat>();
List<Dog> dogList = new ArrayList<Dog>();
// add list of super class Animal of Cat class
addCat(animalList);
// add list of Cat class
addCat(catList);
// 編譯錯(cuò)誤
// addCat(redCatList);
//編譯出錯(cuò)
// addCat.addMethod(dogList);
}
}
2.5 Java 為什么不支持泛型數(shù)組
2.5.1 分析
對(duì)于Java數(shù)組类嗤,數(shù)組必須明確知道內(nèi)部元素的類型遗锣,而且編譯器會(huì)“記住”這個(gè)類型,每次往數(shù)組里插入新元素都會(huì)進(jìn)行類型檢查派殷,類型不匹配會(huì)拋出java.lang.ArrayStoreException
錯(cuò)誤拓轻。如果我們直接創(chuàng)建一個(gè)泛型數(shù)組:
List<String>[] ls = new ArrayList<String>[10];
這樣是沒(méi)有辦法通過(guò)的扶叉。原因就是Java的泛型是通過(guò)類型擦除(type erasure)來(lái)實(shí)現(xiàn)的枣氧。什么是類型擦除呢张弛,簡(jiǎn)單來(lái)說(shuō)Java在編譯期間吞鸭,所有的泛型信息都會(huì)被擦除掉刻剥。
List<String> str = new ArrayList<>();
List<Object> ob = new ArrayList<>();
List list = new ArrayList();
比如上面3個(gè)List造虏,擦除類型參數(shù)以后,List中的實(shí)際元素都是Object撵术。JVM看到的只是List话瞧,而由泛型附加的類型信息對(duì)JVM來(lái)說(shuō)是不可見的划滋。
Java編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方处坪,但是仍然無(wú)法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。所以由于類型擦除的原因部脚,Java是禁止直接創(chuàng)建泛型數(shù)組實(shí)例的丧没。
2.5.2 如何創(chuàng)建真正的泛型數(shù)組
我們不能直接創(chuàng)建泛型數(shù)組呕童,飯可以吧數(shù)組強(qiáng)制轉(zhuǎn)換成泛型類型拉庵。
List<String>[] ls = (List<String>[])new ArrayList[10];
這樣編譯的時(shí)候并不會(huì)報(bào)錯(cuò),因?yàn)镴ava雖然禁止直接創(chuàng)建泛型數(shù)組實(shí)例烁挟,但并沒(méi)有禁止聲明一個(gè)泛型數(shù)組引用撼嗓。所以仍然可以通過(guò)強(qiáng)制轉(zhuǎn)型原生類型數(shù)組的方式,繞過(guò)限制斑芜。
雖然編譯期不會(huì)報(bào)錯(cuò)杏头,但是這樣做仍然有潛在的風(fēng)險(xiǎn)醇王,因?yàn)轭愋筒脸拇嬖冢龅较旅孢@種情況運(yùn)行期間就掛了:
List<String>[] ls = (List<String>[])new ArrayList[10];
ls[0] = new ArrayList(Arrays.asList(new Integer[]{1}));
Java 中不允許直接創(chuàng)建泛型數(shù)組呼渣,所以并不建議通過(guò)各種方式繞過(guò)編譯器的限制排嫌,這樣會(huì)帶來(lái)在代碼出錯(cuò)時(shí)缰犁,編譯器也有可能無(wú)法及時(shí)發(fā)現(xiàn)錯(cuò)誤,從而帶來(lái)潛在的風(fēng)險(xiǎn)伍伤。
參考文章:Java 為什么不支持泛型數(shù)組