從 Java 程序設(shè)計(jì)語(yǔ)言 1.0 版本發(fā)布以來(lái),變化最大的部分就是泛型,致使Java SE 5.0 中增加泛型機(jī)制的主要原因是為了滿足 1999 年制定的最早的 Java 規(guī)范需求之一 (JSR 14)。使用泛型機(jī)制編寫程序代碼要比那些雜亂地使用 Object 變量,然后再進(jìn)行強(qiáng)制類型轉(zhuǎn)換的代碼具有更好的安全性和可讀性。[1]
為什么要使用泛型
泛型程序設(shè)計(jì)(Generic programming)意味著編寫的代碼可以被很多不同類型的對(duì)象所重用,例如 ArrayList 類 可以存放 String 類型對(duì)象,也可以存放 Integer 類型對(duì)象
類型參數(shù)的好處
在 Java 增加泛型類之前,泛型程序設(shè)計(jì)是用繼承實(shí)現(xiàn)的,ArrayList 類只維護(hù)一個(gè) Object 引用的數(shù)組。
public class ArrayList { // before generic classes
private Object[] elementData;
...
public Object get(int i) {}
public void add(Object 0) {}
}
這種方法存在兩個(gè)問(wèn)題:
- 當(dāng)獲取一個(gè)值是必須進(jìn)行強(qiáng)制類型轉(zhuǎn)換:
String filename = (String) files.get(0); - 沒(méi)有錯(cuò)誤檢查,可以向數(shù)組列表中添加任意類型的對(duì)象:
files.add(new File("..."));,對(duì)于這個(gè)調(diào)用,編譯和運(yùn)行都不會(huì)報(bào)錯(cuò),然而在其他地方,如果將 get 的結(jié)果強(qiáng)制類型轉(zhuǎn)換為 String 類型,就會(huì)產(chǎn)生一個(gè)錯(cuò)誤。
泛型提供了更好的解決方案:類型參數(shù)(type parameters): ArrayList<String> files = new ArrayList<String>();
Java SE7 之后,構(gòu)造函數(shù)中可以省略泛型類型:
ArrayList<String> files = new ArrayList<>();
編譯器可以很好地使用類型參數(shù), 當(dāng)調(diào)用 get 時(shí),不用進(jìn)行強(qiáng)制類型轉(zhuǎn)換,編譯器知道返回類型為 String,而且編譯器知道有類型為 String 的 add 方法,會(huì)檢查插入?yún)?shù)的類型是否一致,這些使程序具有更好的可讀性和安全性。
泛型類
一個(gè)泛型類(generic class)就是具有一個(gè)或者多個(gè)類型變量的類,參考 corejava 的示例代碼,我們只關(guān)注泛型,而不會(huì)為數(shù)據(jù)存儲(chǔ)的細(xì)節(jié)煩惱。
public class Pair<T> {
private T first; // use the type variable
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
類定義中的類型變量(type parameters,示例代碼中為T)制定方法的返回類型以及域和局部變量的類型,泛型類可以看成普通類的工廠。
// 自定義泛型類的簡(jiǎn)單使用
public class PairTest1 {
public static void main(String[] args) {
String[] words = {"Mary", "had", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst()); // min = Mary
System.out.println("max = " + mm.getSecond()); // max = lamb
}
}
class ArrayAlg {
public static Pair<String> minmax(String[] a) {
if (a == null || a.length == 0) return null;
String min = a[0];
String max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (min.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
泛型方法
泛型方法可以定義在普通類中,也可以 定義在泛型類中,其中類型變量放在修飾符后面,返回類型前面。
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
String middle = ArrayAlg.<String>getMiddle(words);
String middle = ArrayAlg.getMiddle(words); // 編譯器可以 自動(dòng)判斷出T的類型。
類型變量的限定
有時(shí)需要對(duì)類或方法對(duì)泛型參數(shù)進(jìn)行限定,此時(shí)可以通過(guò)使用通配符來(lái)解決。
public class PairTest2 {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1906, 12, 9),
LocalDate.of(1985, 3, 5),
LocalDate.of(1406, 2, 4),
LocalDate.of(1996, 6, 7),
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i]; // min = 1406-02-04
if (min.compareTo(a[i]) < 0) max = a[i]; // max = 1996-06-07
}
return new Pair<>(min, max);
}
}
<T extends BoundingType> 表示 T 應(yīng)該是綁定類型的子類型, T 和綁定類型可以是類,也可以是接口,選擇關(guān)鍵字 extends 的原因是更接近子類的概念。一個(gè)變量或通配符可以有多個(gè)限定,如 T, U extends Comparable. & Serializable。在 Java 繼承中,可以選擇多個(gè)接口超類型,但限定中至多有一個(gè)類,如果用一個(gè)類作為限定,它必須是限定列表 中的第一個(gè)。
泛型代碼和虛擬機(jī)
虛擬機(jī)沒(méi)有泛型類型對(duì)象——所有對(duì)象都屬于普通類。
類型擦除
無(wú)論何時(shí)定義一個(gè)泛型類型,都自動(dòng)提供了一個(gè)相應(yīng)的原始類型(raw type),原始類型的名字就說(shuō)是刪去類型參數(shù)后的反響類型名。擦除(erased)類型變量,并替換為限定類型(無(wú)限定的變量用Object),如之前在繼承與多態(tài)中用的示例代碼,在擦除后的原始類型如下:
public class Pair {
private Object first;
private Object second;
public Pair2(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
}
因?yàn)?T 是一個(gè)無(wú)限定變量,所以直接用 Object 替換。在程序中可以包含不同類型的 Pair,如 Pair<String> 和 Pair<LocalDate>, 而擦除類型之后就變成原始的 Pair 類型。
泛型擦除的規(guī)則為:原始類型用第一個(gè)限定的類型變量來(lái)替換,如果沒(méi)有給定限定就用 Object 來(lái)替換。我們看下面的例子。
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
public Interval(T lower, T upper) {
if (lower.compareTo(upper) > 0) {
this.lower = lower;
this.upper = upper;
} else {
this.upper = lower;
this.lower = upper;
}
}
}
在泛型擦除之后,原始類型如下:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable lower, Comparable upper) {
if (lower.compareTo(upper) > 0) {
this.lower = lower;
this.upper = upper;
} else {
this.upper = lower;
this.lower = upper;
}
}
}
如果寫出 class Interval<T extends Serializable & Comparable>,那么原始類型會(huì)用 Serializable 來(lái)代替 T,而編譯器在必要時(shí)要向 Comparable 插入強(qiáng)制類型轉(zhuǎn)換。所以為了提高效率,應(yīng)該將標(biāo)簽(tagging)接口(即沒(méi)有方向的接口)放在邊界列表的末尾。
翻譯泛型表達(dá)式
當(dāng)程序調(diào)用泛型方法時(shí),如果擦除返回類型,編譯器會(huì)插入強(qiáng)制類型轉(zhuǎn)換,例如我們的Pair<T>
Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();
調(diào)用 getFirst 方法時(shí)編譯器把這個(gè)方法調(diào)用翻譯為兩條虛擬機(jī)指令:
- 對(duì)原始方法 Pair.getFirst 的調(diào)用
- 將返回的 Object 類型強(qiáng)制轉(zhuǎn)換為 Employee 類型
同樣的情況也會(huì)出現(xiàn)在存取一個(gè)泛型域時(shí),如
Employee buddy = buddies.first;
翻譯泛型方法
類型擦除也會(huì)出現(xiàn)在泛型方法中,我們看以下泛型方法的定義:
public static <T extends Comparable> T min(T[] a)
泛型擦除后:
public static Comparable min(Comparable[] a)
我們可以看到參數(shù) T 已經(jīng)被擦除了,只留下了限定類型 Comparable,方法的擦除會(huì)帶來(lái)兩個(gè)巨大的問(wèn)題。這里使用 oracle tutorial 中的代碼來(lái)講解[2]。
// before erasure
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) {
super(data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
// after erasure
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
MyNode 是 Node 的子類,并且它復(fù)寫了父類的 setData 方法。當(dāng)我們嘗試調(diào)用以下代碼。
// after erasure
MyNode mn = new MyNode(1);
Node n = mn;
n.setData("Hello");
Integer x = mn.data;
// before erasure
MyNode mn = new MyNode(1);
Node n = (MyNode) mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Integer x = (String) mn.data;
看到上述代碼時(shí),第一反應(yīng)是為什么n.setData("Hello");不會(huì)報(bào)錯(cuò),按照多態(tài)的原理,此時(shí)會(huì)調(diào)用MyNode.setData(Integer data) 方法,放入一個(gè) String 為什么編譯器會(huì)沒(méi)報(bào)錯(cuò)?
先引用官方的解釋
After type erasure, the method signatures do not match. The
Nodemethod becomessetData(Object)and theMyNodemethod becomessetData(Integer). Therefore, theMyNodesetDatamethod does not override theNodesetDatamethod.
因?yàn)榇嬖诜盒筒脸?/strong>,在擦除后 Node 中的方法變?yōu)?Node.setData(Object data) , 很明顯 Object 與 Integer 不是同一個(gè)類型,所以 MyNode 中同時(shí)存在從 Node 中繼承過(guò)來(lái)的 Node.setData(Object data)方法,n.setData("Hello"); 實(shí)際上調(diào)用的 Node.setData(Object data), 所以在編譯時(shí)期能檢查通過(guò),這行代碼運(yùn)行時(shí)發(fā)生如下操作:
- 調(diào)用 MyNode 類中的 setData(Object) (這個(gè)方法會(huì)被編譯器自動(dòng)改寫為橋方法)方法(因?yàn)?MyNode 從 Node 中繼承了 setData(Object)方法)
- n 引用的對(duì)象中的 data 域 被賦值為 "Hello"
- mn 引用與 n 同一個(gè)對(duì)象,但是這里的 data 域被期望為 Integer 類型,因?yàn)?mn 是
MyNode extends Node<Integer>的對(duì)象,此時(shí)因?yàn)闃蚍椒ㄖ械膹?qiáng)制類型轉(zhuǎn)換而拋出ClassCastException,程序結(jié)束。
橋方法(Birdge Method)
根據(jù)多態(tài)的設(shè)計(jì)初衷,n.setData("Hello"); 應(yīng)該調(diào)用 MyNode.setData(Integer data),但是最終它調(diào)用的卻是 Node.setData(Object data),我們可以看出類型擦除(type erasure) 與多態(tài)(polymorphism) 之間存在沖突,為了保證多態(tài)的可用性,Java 編譯器會(huì)自動(dòng)生成橋方法來(lái)解決這個(gè)問(wèn)題, 如 MyNode 將會(huì)變成如下代碼。
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
正是因?yàn)橛袠蚍椒ǖ拇嬖?,才能保證多態(tài)功能的正常使用。
橋方法還可以用在其他地方,比如一個(gè)方法覆蓋另一方法是可以指定一個(gè) 更嚴(yán)格的返回類型。
public class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException {
Employee clone = (Employee) super.clone();
return clone;
}
}
這里其實(shí) Employee 有兩個(gè) clone 方法
Employee clone();
Object clone();
此時(shí)也需要編譯器合成橋方法,合成的橋方法調(diào)用了新定義的方法。
總結(jié)
- 虛擬機(jī)中沒(méi)有泛型,只有普通的類和方法
- 所有的類型參數(shù)都用他們的限定類型替換
- 橋方法被用來(lái)合成保持多態(tài)
- 為保持類型安全性,必要時(shí)插入強(qiáng)制類型轉(zhuǎn)換。
約束與局限性
使用泛型時(shí)也有一些約束與局限性,大部分的約束都是由類型擦除引起的。
不能用基本類型實(shí)例化類型參數(shù):如不能
Pair<double>,只能Pair<Double>,原因很簡(jiǎn)單,當(dāng)類型擦除之后,只剩下 Object 類型的域, 而 Object 不能存儲(chǔ) double 的值,這樣做與 Java 語(yǔ)言中基本類型的獨(dú)立狀態(tài)相一致。-
運(yùn)行時(shí)類型查詢只適用于原始類型:所有的類型查詢只產(chǎn)生原始類型,如
if (a instanceof Pair<String>) // Error if (a instanceof Pair<T>) // Error Pair<String> stringPair = new Pair<>(); Pair<Employee> employeePair = new Pair<>(); stringPair.getClass() == employeePair.getClass // true getClass方法總返回原始類型 不能創(chuàng)建參數(shù)化類型的數(shù)組:
Node<String>[] node = new Node<String>[10]; // Error,因?yàn)轭愋筒脸?node 的類型變成Node[],可以把它轉(zhuǎn)化為Object[] objArray = node;,數(shù)組會(huì)記住它的元素類型,如果試圖存儲(chǔ)其他的類型,如objArray[0] = "hello";,就會(huì)拋出ArrayStoreException異常。Varargs警告:
public static <T> void addAll(Collection<T> coll, T... ts)這個(gè)方法定義會(huì)拋出警告,因?yàn)槠渲械囊粋€(gè)參數(shù)為可變參數(shù),本質(zhì)上是泛型數(shù)組,這就違反了上一條規(guī)則,Java SE 7后可以使用@SafeVarargs進(jìn)行消除警告。-
不能實(shí)例化類型變量:
不能使用
new T(...), new T[...], T.class這些表達(dá)式,也不能使用如下構(gòu)造器:public Pair<T> { first = new T(); second = new T(); }比較好的解決方法為使用構(gòu)造器表達(dá)式,如
public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get(), constr.get()); } // 調(diào)用 Pair<String> p = Pari.makePair(String::new);比較傳統(tǒng)的方法是通過(guò)反射調(diào)用 Class.newInstance 方法來(lái)構(gòu)造泛型對(duì)象
first = T.class.newInstance(); // Error,因?yàn)榇嬖诜盒筒脸?T.class會(huì)被擦除為 Object.class public static <T> Pair<T> makePair(Class<T> c1) { try { return new Pair<>(c1.newInstance(), c1.newInstance()); } catch(Exception ex) { return null; } } // 調(diào)用 Pair<String> p = Pari.makePair(String.class); 泛型類的靜態(tài)上下文中類型變量無(wú)效:即不能在靜態(tài)域或方法中引用類型變量。
不能拋出和捕獲泛型類的實(shí)例
可以消除對(duì)受查異常的檢查
注意擦除后的沖突
泛型類的繼承
若 Manager 是 Employee 的子類,那么 Pair<Manager> 不是 Pair<Employee> 的子類,這一限制主要是出于類型安全的考慮,考慮一下代碼:
Pair<Manager> manager = new Pair<>(cto, cfo);
Pair<Employee> employee = manager;
employee.setFirst(staff); // 將普通員工與管理者放在一起明顯破壞了程序設(shè)計(jì)的意圖
永遠(yuǎn)可以將一個(gè)參數(shù)化類型轉(zhuǎn)化為一個(gè)原始類型,例如 Pair<Employee> 是原始類型的子類型,并且泛型類可以擴(kuò)展或?qū)崿F(xiàn)其他的泛型類,如 ArrayList<T> 實(shí)現(xiàn) List<T> 接口,這意味著一個(gè) ArrayList<Manager> 可以轉(zhuǎn)換為List<Manager>, 但是一個(gè) ArrayList<Manager>與 ArrayList<Employee>或 List<Employee>之間沒(méi)有關(guān)系。
通配符類型
public static void test(Pair<? extends Employee> test) // 表示任何泛型 Pair 類型,它的類型參數(shù)是 Employee 的子類 以及其本身。
public static void test(Pair<? super Employee> test) // 表示任何泛型 Pair 類型,它的類型參數(shù)是 Employee 的父類 以及其本身。
直觀得講,帶有超類型限定的通配符可以向泛型對(duì)象寫入(可以用構(gòu)造器方法),帶有子類型限定的通配符對(duì)象可以從泛型對(duì)象讀取。[1]
Pair<?> 無(wú)限定通配符
本文章與github上同步,歡迎來(lái)玩,提交issue。