泛型程序設(shè)計 (Generic programming) 意味著編寫的代碼可以被很多不同的類型的對象所重用。
原始類型(Raw Type)
下面我們會用一些例子來說明,為什么使用泛型編寫的代碼可以被不同類型的對象所重用。在沒有泛型之前 ArrayList 的代碼是這樣的:
public class RawArrayList {
private Object[] element;
public Object getElement(int position) {
//...
}
public void addElement(Object element) {
//...
}
}
public class RawArrayListTest {
public static void main(String[] args) {
RawArrayList rawArrayList = new RawArrayList();
rawArrayList.addElement("string");
rawArrayList.addElement(new File("file"));
String string = (String) rawArrayList.getElement(0);
//此處會出現(xiàn)類型轉(zhuǎn)換異常
String file = (String) rawArrayList.getElement(1);
}
}
上面的代碼有兩個問題:
- 編譯器沒有錯誤檢查,我們可以調(diào)用 setElement("string") 方法向 RawArrayList 中放入一個 String 類型的字符串,之后我仍然可以向其中放入一個其他類型的對象,例如 setElement(new File("file"))。編譯器并不會有任何警告。
- 因為 RawArrayList 內(nèi)部使用 Object 數(shù)組 來存儲對象,這樣我們在獲取對象的時候就必須使用強制類型轉(zhuǎn)換,
String string = (String) rawArrayList.getElement(0);,由于 RawArrayList 沒有對放入的類型做限制,所以就有可能出現(xiàn)類型轉(zhuǎn)換異常 java.lang.ClassCastException。
類型參數(shù)
泛型提供了類型參數(shù) (type parameters) 來幫助我們改善上述的代碼。
可以認(rèn)為是給 RawArrayList 中聲明一個參數(shù),這個參數(shù)就代表著列表中元素的類型,我們會在聲明 RawArrayList 的時候指明參數(shù)的具體類型。
類型參數(shù)用尖括號加任意字母表示 : <T>,字母一般為單個大寫字母,并有一定含義,例如 T(type),E(element),K(key),V(value) 等等
下面來看看用使用類型參數(shù)之后的 ArrayList:
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<String> stringList = new ArrayList<String>();
stringList.add("string");
// 下面一行代碼,編譯器會報錯,無法將 File 對象應(yīng)用于 String 類型的 ArrayList
// stringList.add(new File("file"));
String string=stringList.get(0);
System.out.println(string);
}
// print > string
}
ArrayList<String> stringList = new ArrayList<String>(); 這一行代碼中,可以省略創(chuàng)建 ArrayList 對象的時候傳遞的參數(shù)類型如 ArrayList<String> stringList = new ArrayList<>();,編譯器可以從聲明中推斷出省略的類型。
注意尖括號不能省略,不然可能造成類型不安全的隱患( 這相當(dāng)于將原始類型的對象傳遞給泛型類型的引用 )。例如我們將之前的內(nèi)部具有 String 類型和 File 對象的 RawArrayList 傳遞給 ArrayList<String> 類型的引用,會造成什么影響呢?
RawArrayList list=new RawArrayList();
list.add(1);
list.add("string");
ArrayList<String> stringArrayList=list;
for (String s : stringArrayList) {
System.out.println(s);//boom
}
很不幸會發(fā)生類型轉(zhuǎn)換異常,我們最好不要將原生類型和泛型類型這樣使用,除非你能保證類型安全。
泛型的一個目的就是盡早的發(fā)現(xiàn)可能出現(xiàn)的異常,在使用了泛型提供的類型參數(shù)之后,有兩個顯著的好處是
- 我們不需要自己進行類型轉(zhuǎn)換了,編譯器能推斷出返回類型,可讀性提高
- 編譯器會對插入數(shù)據(jù)做類型檢查,避免插入了錯誤的類型,安全性提高
好像 RawArrayList 代碼的例子沒有明顯體現(xiàn)出我們在開頭所說的,泛型代碼可以被很多不同的類型的對象所重用。
接下來在讓我們看看水果和果盤的例子:
Apple
public class Apple {
private String name;
public Apple(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Orange
public class Orange {
private String name;
public Orange(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
現(xiàn)在我們再來定義一個果盤,用來存放水果。我們需要定義一個蘋果果盤,用來存放蘋果,再定義一個橘子果盤,用來存放橘子。
ApplePlate
public class ApplePlate {
private List<Apple> appleList;
public ApplePlate(List<Apple> appleList) {
this.appleList = appleList;
}
public void setAppleList(List<Apple> appleList) {
this.appleList = appleList;
}
public List<Apple> getAppleList() {
return appleList;
}
}
OrangePlate
public class OrangePlate {
private List<Orange> orangePlate;
public OrangePlate(List<Orange> orangePlate) {
this.orangePlate = orangePlate;
}
public List<Orange> getOrangePlate() {
return orangePlate;
}
public void setOrangePlate(List<Orange> orangePlate) {
this.orangePlate = orangePlate;
}
}
將蘋果放進蘋果果盤
private static void createApple() {
//生成蘋果
List<Apple> apples = new ArrayList<>();
Apple apple1 = new Apple("蘋果1");
Apple apple2 = new Apple("蘋果2");
Apple apple3 = new Apple("蘋果3");
apples.add(apple1);
apples.add(apple2);
apples.add(apple3);
//將蘋果放入蘋果果盤
ApplePlate applePlate = new ApplePlate(apples);
//取出剛放入的蘋果們
for (Apple apple : applePlate.getAppleList()) {
System.out.println(apple.getName());
}
}
現(xiàn)在將橘子放進橘子果盤的話,只需要按照 createApple() 方法在編寫一個 createOrange() 就可以了。
那如果現(xiàn)在我要新增一個水果類型怎么辦,我還需要對應(yīng)的再增加一個該水果類型的果盤。而且可以看到,我們水果的屬性,方法,果盤的方法,除了類型不同之外,沒什么不同。這時候就可以使用泛型來解決這個問題。
泛型類/接口
先讓我們看看泛型類的概念:
具有一個或者多個類型參數(shù)的類/接口,就是泛型類/泛型接口
在定義類的時候,我們在類名的后面加上一個形如 <T> 的類型參數(shù)。類中屬性的聲明,方法的參數(shù)類型,包括返回類型等,都可以用類型 T 替代。泛型接口與泛型類的定義相同,我們就不展開敘述了。
現(xiàn)在我們將果盤(XXXPlate)改寫為泛型類是什么樣子
public class Plate<T> {
private List<T> fruitList;
public Plate(List<T> fruitList) {
this.fruitList = fruitList;
}
public List<T> getFruitList() {
return fruitList;
}
public void setFruitList(List<T> fruitList) {
this.fruitList = fruitList;
}
}
現(xiàn)在我們先抽象出一個水果類 Fruit
public class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
為了對比,我們新創(chuàng)建一種水果類型 Cherry 使他繼承 Fruit
public class Cherry extends Fruit {
public Cherry(String name) {
super(name);
}
}
首先通過抽象將共有的屬性和方法抽象到父類中去,這樣子類只需要實現(xiàn)一個構(gòu)造函數(shù)即可。
接下來我們看看,使用了泛型之后,是如何將 Cherry 裝進果盤中去的。
public static void createCherry() {
//生成車?yán)遄? List<Cherry> cherryList = new ArrayList<>();
Cherry cherry1 = new Cherry("車?yán)遄?");
Cherry cherry2 = new Cherry("車?yán)遄?");
Cherry cherry3 = new Cherry("車?yán)遄?");
cherryList.add(cherry1);
cherryList.add(cherry2);
cherryList.add(cherry3);
//將剛買的車?yán)遄臃湃胲嚴(yán)遄庸P
Plate<Cherry> cherryPlate = new Plate<>(cherryList);
for (Cherry cherry : cherryPlate.getFruitList()) {
System.out.println(cherry.getName());
}
}
假如我們現(xiàn)在又增加了一種水果 Pear ,這個時候我們只需要將 Plate<T> 中的類型參數(shù)指定為 Pear 這個樣子 Plate<Pear> 即可。
這就體現(xiàn)了我們上面所說的,泛型代碼可以被不同類型的對象所重用??梢赃@么認(rèn)為:我們封裝了一套數(shù)據(jù)結(jié)構(gòu)和算法,用來處理一類操作,他與具體的類型無關(guān),或者與限定的類型有關(guān),這個時候,我們就可以使用泛型,只關(guān)注具體的操作,不用關(guān)心具體的類型。
泛型方法
我們類比泛型類可以知道,泛型方法就是具有一個或者多個類型參數(shù)的方法
將類型參數(shù) <T> 放在修飾符的后面,返回類型的前面,這樣我們的返回值,方法中的局部變量,參數(shù)類型都可以指定為我們聲明的 T 類型。
我們這樣定義一個泛型方法:
public static <T> T getMiddleFruit(Plate<T> plate) {
int middle = plate.getFruitList().size();
return plate.getFruitList().get(middle);
}
這段代碼的意思是,獲取 Plate<T> 中間的元素,也就是獲取果盤最中間的水果。
我們可以這樣來調(diào)用它:
public static void createCherry() {
/...省略之前創(chuàng)建水果,將水果放進果盤的操作
Cherry middleCherry=PlateUtils.getMiddleFruit(cherryPlate);
System.out.println(middleCherry.getName());
}
泛型限定
在回頭看我們上面定義的泛型類 Plate<T>,我們的類型參數(shù)是沒有做任何限定的,類型參數(shù) T 可以在聲明的時候被指定為任何類型。
雖然我將 Plate,定義為果盤,可以傳進來任何類型的水果,但其實由于我沒有對 T 做任何的限定,那就意味著我們在聲明的時候可以傳遞任意類型。
如下我們定義一個動物類型 Animal
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后我們來嘗試將一群動物放進果盤中:
public static void createAnimal() {
List<Animal> animalList = new ArrayList<>();
Animal dog = new Animal("Dog");
Animal cat = new Animal("Cat");
animalList.add(dog);
animalList.add(cat);
Plate<Animal> animalPlate = new Plate<>(animalList);
for (Animal animal : animalPlate.getFruitList()) {
System.out.println(animal.getName());
}
}
尷尬??,我們從果盤中取出一群動物,現(xiàn)在我們要修正上面的代碼,不然就亂套了,這個時候我就可以使用類型限定,對類型參數(shù)加以約束。
我們只需要將代碼改成這樣:
public class Plate<T extends Fruit> {...}
將類型限定為 Fruit 類型或者是 Fruit 的子類,這樣我們在嘗試編譯上面的createAnimal()代碼的時候,編譯器就會報錯:
- Error:(172, 15) java: 類型參數(shù)model.Animal不在類型變量T的范圍內(nèi)
- Error:(172, 37) java: 無法推斷model.plate.Plate<>的類型參數(shù)
原因: 推論變量 T 具有不兼容的限制范圍
等式約束條件: model.Animal
上限: model.base.Fruit
由于我們對 Plate 能夠接收的類型做了限制,所以現(xiàn)在我們無法將一群動物 List<Animal> 放進果盤了。
泛型擦除
Java 核心技術(shù):無論何時定義一個泛型類型,都會自動提供一個原始類型(Raw Type),原始類型的名字就是刪去類型參數(shù)后的泛型類型名。擦除類型變量,并切換為限定類型,沒有限定類型則替換為 Object
請看下面的代碼:
List<Banana> bananas = new ArrayList<>();
Plate<Banana> bananaPlate = new Plate<>(bananas);
List<Pear> pears = new ArrayList<>();
Plate<Pear> pearPlate = new Plate<>(pears);
System.out.println(bananaPlate.getClass());
System.out.println(pearPlate.getClass());
System.out.println(bananaPlate.getClass()==pearPlate.getClass());
//print>class model.plate.Plate
//print>class model.plate.Plate
//print>true
在運行時,Plate<Banana> 和 Plate<Pear> 的類型是一樣的,都是 Plate 類型,我們在代碼中指定的具體類型 Banana 和Pear 不見了。
造成這個問題的原因就是類型擦除,類型擦除后的 Plate<T extends Fruit> 代碼是這個樣子的,為了展示類型變量被替換成為限定類型,我特意在原有的 Plate 的代碼中加入一句 ``T t;`,聲明一個 T 類型的變量 t,然后我們來看擦除后的代碼:
public class Plate {
private Fruit t;
private List fruitList;
public Plate(List fruitList) {
this.fruitList = fruitList;
}
public List getFruitList() {
return fruitList;
}
public void setFruitList(List fruitList) {
this.fruitList = fruitList;
}
}
可以看到在運行時所有有關(guān)于泛型的信息全部不見了,使用泛型聲明的屬性,全都替換成了限定類型 Fruit(大家可以嘗試用 javap -c -s -p Plate
指令來反編譯字節(jié)碼,就可以看到更具體的信息)
在文章的最開始我們舉了一個有關(guān)于 ArrayList 的例子,聲明一個 String 類型的 ArrayList,我們先 add 一個字符串進去,在將該字符串取出,來看看編譯后的文件
...
ArrayList<String> stringList = new ArrayList<>();
stringList.add("string");
String string=stringList.get(0);
...

- 創(chuàng)建了一個原生的 ArrayList 沒有泛型相關(guān)的信息
- 調(diào)用 add 方法,接收的參數(shù)類型是 Object 類型
- 調(diào)用 get 方法,方法的返回類型是 Object 類型
- 將 Object 類型轉(zhuǎn)化為 String 類型
Java 中的泛型之所以設(shè)計成編譯時泛型,就是為了兼容老代碼,能夠和之前的代碼相互操作
拿我們的 Plate 來說說這件事,來看看原生的 Plate 類型和泛型類 Plate<T> 之間是怎么相互操作并且會帶來什么影響。
public class RawAndGenericOperation {
public static void main(String[] args) {
List<Banana> bananas = new ArrayList<>();
Banana banana = new Banana("banana");
bananas.add(banana);
Plate<Banana> bananaPlate = new Plate<>(bananas);
Plate rawPlate = bananaPlate;
List<Pear> pears = new ArrayList<>();
Pear pear = new Pear("pear");
pears.add(pear);
rawPlate.setFruitList(pears);//
for (Object o : rawPlate.getFruitList()) {
System.out.println(((Banana) o).getName());
//Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
}
}
}
在 For 循環(huán)取數(shù)據(jù)的時候??了,類型轉(zhuǎn)換異常,代碼清晰可見,是由于我們自己將 List<Pear> 傳遞給了 Plate,原生類型沒有類型檢查,造成了類型不安全的隱患,我們在將泛型類型對象傳遞給原生類型的引用的時候,這個隱患就存在了,誰知道它們會對原生類型做些什么呢。
在來看看將原生類型的對象,傳遞給泛型類型的引用:
public class RawAndGenericOperation {
public static void main(String[] args) {
List bananaList = new ArrayList();
bananaList.add(new Banana("banana"));
bananaList.add(new Pear("pear"));
List<Banana> bananas = new ArrayList<>();
bananas = bananaList;
for (Banana banana : bananas) {
System.out.println(banana.getName());
//Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
}
}
}
依然??,由于原生類型的 List 沒有對存儲的元素做限制,我們在 BananaList 中混入了 Pear,然后將它賦值給 List<Banana>。在循環(huán)的時候出現(xiàn)了類型轉(zhuǎn)換異常,除非你能保證原生 List 中的元素類型和泛型類型保持一致,不然就不要這么做。
但是在與遺留的代碼進行銜接的時候,難免會出現(xiàn)上述的情況,但是沒有關(guān)系,這里只是失去了泛型程序設(shè)計提供的附加安全性,不會變的更壞了。
泛型的繼承關(guān)系
在之前我們定義了 Banana 類型,它是 Fruit 的子類。那么 List<Banana> 是 List<Fruit> 的子類嗎?那么 Banana[] 是 Fruit[] 類型的子類嗎?
在回答這兩個問題之前,我們先來看看一個概念 Java中的逆變與協(xié)變。然后我們來寫兩段代碼試試看:
public class Covariant {
public static void main(String[] args) {
List<Fruit> fruitList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();
//編譯器提示類型不兼容
fruitList = bananaList;
Fruit[] fruits = new Fruit[10];
Banana[] bananas = new Banana[10];
//不會有任何問題
fruits = bananas;
}
}
由上可知 List<Banana> 沒有辦法轉(zhuǎn)化成 List<Fruit> 類型。但是 Banana[] 可以轉(zhuǎn)化為 Fruit[] 類型,劃重點在 Java 中數(shù)組是支持協(xié)變的,但是泛型是不支持協(xié)變的。假如讓泛型支持協(xié)變會怎么樣,假設(shè) List<Banana> 可以傳遞給 List<Fruit> 類型的引用會發(fā)什么呢?
List<Fruit> fruitList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();
//假設(shè)這行代碼允許執(zhí)行
fruitList = bananaList;
fruitList.add(new Pear("pear"));
fruitList.add(new Fruit("fruit"));
for (Banana banana : bananaList) {
//類型轉(zhuǎn)換異常
}
和上面的問題一樣,我們丟失了類型的安全性。那為什么數(shù)組是協(xié)變的但卻一點事情沒有呢?
首先來看看知乎中胖胖的回答:Java 中數(shù)組為什么要設(shè)計為協(xié)變
public class CovariantArray {
public static void main(String[] args) {
Fruit[] fruits = new Fruit[3];
Banana[] bananas = new Banana[4];
bananas[0] = new Banana("banana1");
bananas[1] = new Banana("banana2");
bananas[2] = new Banana("banana3");
fruits = bananas;
fruits[3] = new Pear("pear");
for (Fruit fruit : fruits) {
System.out.println(fruit.getName());
//Exception in thread "main" java.lang.ArrayStoreException: model.pear.Pear
}
}
}
數(shù)組中帶有特別的保護,數(shù)組會在創(chuàng)建的時候記住元素類型,如果后續(xù)的插入與之前的類型不匹配,虛擬機將會拋出 ArrayStoreException 異常。
數(shù)組在插入的時候就暴露出問題,如果是泛型協(xié)變的話,你就不知道什么時候會發(fā)現(xiàn)問題了。
泛型數(shù)組
在最開始你可能會編寫這樣一段代碼:
public class GenericArray {
public static void main(String[] args) {
Plate<Banana>[] bananaArray = new Plate<Banana>[10];
}
}
編譯器會直接報 Error:創(chuàng)建泛型數(shù)組,我們是沒有辦法通過 new 的方式來創(chuàng)建泛型數(shù)組的,如果編譯器允許我們創(chuàng)建泛型數(shù)組會怎么樣?
public class GenericArray {
public static void main(String[] args) {
// 假設(shè)編譯器沒有報錯
Plate<Banana>[] bananaArray = new Plate<Banana>[2];
List<Banana> bananas = new ArrayList<>();
Plate<Banana> bananaPlate = new Plate<>(bananas);
bananaArray[0] = bananaPlate;
List<Pear> pears = new ArrayList<>();
Plate<Pear> pearPlate = new Plate<>(pears);
Object[] objectArray = bananaArray;
objectArray[1] = pearPlate;
}
}
可以看到我們將 Plate<Banana>[] 向上轉(zhuǎn)型為 Object[],然后向其中追加一個 Plate<Pear>對象,這個時候編譯器沒有報錯,原本我們期望在運行時,數(shù)組會判斷出加入的數(shù)據(jù)類型不對從而報出 ArrayStoreException,但是被忘了類型擦除這回事,我們的 Plate<T> 全部被擦除成 Plate 類型,對于數(shù)組來說,無論你插入 Plate<Banana> 或者 Plate<Pear> 由于類型擦除,它都認(rèn)為是同一種類型。這個時候泛型數(shù)組就變得類型不安全了。
編譯器只是不允許通過 new Plate<T>[] 這種方式創(chuàng)建數(shù)組,我們依然可以通過其他方式來得到一個泛型數(shù)組,這里就不再介紹了
你總是可以將一個泛型類型轉(zhuǎn)化為原生類型。
具體的示例在上面我們講述與遺留代碼相互調(diào)用的時候已經(jīng)展示過了,相信你已經(jīng)能夠分析出為什么轉(zhuǎn)化為原生類型是不安全的。
泛型類是可以擴展或者實現(xiàn)其他的泛型類的。
就像我們上面一直寫的那樣,ArrayList<Banana> 是可以賦值給 List<Banana> 類型的引用的。