1、為什么引入泛型
bug是編程的一部分,我們只能盡自己最大的能力減少出現(xiàn)bug的幾率,但是誰也不能保證自己寫出的程序不出現(xiàn)任何問題。
錯誤可分為兩種:編譯時錯誤與運行時錯誤。編譯時錯誤在編譯時可以發(fā)現(xiàn)并排除,而運行時錯誤具有很大的不確定性,在程序運行時才能發(fā)現(xiàn),造成的后果可能是災(zāi)難性的。
使用泛型可以使錯誤在編譯時被探測到,從而增加程序的健壯性。
來看一個例子:
public class Box {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
按照聲明,其中的set()方法可以接受任何java對象作為參數(shù)(任何對象都是Object的子類),假如在某個地方使用該類,set()方法預(yù)期的輸入對象為Integer類型,但是實際輸入的卻是String類型,就會拋出一個運行時錯誤,這個錯誤在編譯階段是無法檢測的。例如:
Box box = new Box;
box.set("abc");
Integer a = (Integer)box.get(); //編譯時不會報錯,但是運行時會報ClassCastException
運用泛型改造上面的代碼:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
當(dāng)我們使用該類時會指定T的具體類型,該類型參數(shù)可以是類、接口、數(shù)組等,但是不能是基本類型。
比如:
Box<Integer> box = new Box<Integer>; //指定了類型類型為Integer
//box.set(“abc”); 該句在編譯時就會報錯
box.set(new Integer(2));
Integer a = box.get(); //不用轉(zhuǎn)換類型
可以看到,使用泛型還免除轉(zhuǎn)換操作。
在引入泛型機制之前,要在方法中支持多個數(shù)據(jù)類型,需要對方法進行重載,在引入范型后,可以更簡潔地解決此問題,更進一步可以定義多個參數(shù)以及返回值之間的關(guān)系。
例如
public void write(Integer i, Integer[] ia);
public void write(Double d, Double[] da);
public void write(Long l, Long[] la);
范型版本為:
public <T> void write(T t, T[] ta);
總體來說,泛型機制能夠在定義類、接口、方法時把“類型”當(dāng)做參數(shù)使用,有點類似于方法聲明中的形式參數(shù),如此我們就能通過不同的輸入?yún)?shù)來實現(xiàn)程序的重用。不同的是,形式參數(shù)的輸入是值,而泛型參數(shù)的輸入是類型。
2、命名規(guī)則
類型參數(shù)的命名有一套默認規(guī)則,為了提高代碼的維護性和可讀性,強烈建議遵循這些規(guī)則。JDK中,隨處可見這些命名規(guī)則的應(yīng)用。
- E - Element (通常代表集合類中的元素)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. – 第二個,第三個,第四個類型參數(shù)……
注意,父類定義的類型參數(shù)不能被子類繼承。
也可以同時聲明多個類型變量,用逗號分割,例如:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
下面的兩行代碼創(chuàng)建了OrderedPair對象的兩個實例。
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
//也可以將new后面的類型參數(shù)省略,簡寫為:
//Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
//也可以在尖括號內(nèi)使用帶有類型變量的類型變量,例如:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
泛型是JDK 5.0之后才引入的,為了兼容性,允許不指定泛型參數(shù),但是如此一來,編譯器就無法進行類型檢查,在編程時,最好明確指定泛型參數(shù)。
同樣,在方法中也可是使用泛型參數(shù),并且該參數(shù)的使用范圍僅限于方法體內(nèi)。例如:
public class Util {
//該方法用于比較兩個Pair對象是否相等。
//泛型參數(shù)必須寫在方法返回類型boolean之前
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
//實際上,編譯器可以通過Pair當(dāng)中的類型來推斷compare需要使用的類型,所以可以簡寫為:
// boolean same = Util. compare(p1, p2);
有時候我們想讓類型參數(shù)限定在某個范圍之內(nèi),就需要用到extends關(guān)鍵字(extends后面可以跟一個接口,這里的extends既可以表示繼承了某個類,也可以表示實現(xiàn)了某個接口),例如,我們想讓參數(shù)是數(shù)字類型:
class Box<T extends Number> { //類型參數(shù)限定為Number的子類
private T t;
public Box(T t) {
this.t = t;
}
public void print() {
System.out.println(t.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> box1 = new Box<Integer>(new Integer(2));
box1.print(); //打印結(jié)果:java.lang.Integer
Box<Double> box2 = new Box<Double>(new Double(1.2));
box2.print(); //打印結(jié)果:java.lang.Double
Box<String> box2 = new Box<String>(new String("abc")); //報錯,因為String類型不是Number的子類
box2.print();
}
}
如果加入多個限定,可以用“&”連接起來,但是由于java是單繼承,多個限定中最多只能有一個類,而且必須放在第一個位置。例如:
class Box<T extends Number & Cloneable & Comparable> {
//該類型必須為Number的子類并且實現(xiàn)了Cloneable接口和Comparable接口。
//……
}
3、泛型類的繼承
java是面向?qū)ο蟮母呒壵Z言,在一個接受A類參數(shù)的地方傳入一個A的子類是允許的,例如:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // 因為Integer是Object的子類
這種特性同樣適用于類型參數(shù),例如:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // Integer是Number的子類
box.add(new Double(10.1)); // Double同樣是Number的子類
但是,有一種情況很容易引起混淆,例如:
//該方法接受的參數(shù)類型為Box<Number>
public void boxTest(Box<Number> n) {
//……
}
//下面兩種調(diào)用都會報錯
boxTest(Box<Integer>);
boxTest(Box<Double>);
雖然Integer和Double都是Number的子類,但是Box<Integer>與Box<Double>并不是Box<Number>的子類,不存在繼承關(guān)系。Box<Integer>與Box<Double>的共同父類是Object。

以JDK中的集合類為例,ArrayList<E> 實現(xiàn)了 List<E>接口,List<E>接口繼承了 Collection<E>接口,所以,ArrayList<String>是List<String>的子類,而非List<Integer>的子類。三者的繼承關(guān)系如下:

4、類型推斷
先來看一個例子:
public class Demo {
static <T> T pick(T a1, T a2) {
return a2;
}
}
靜態(tài)方法pick()在三個地方使用了泛型,分別限定了兩個輸入?yún)?shù)的類型與返回類型。調(diào)用該方法的代碼如下:
Integer ret = Demo.<Integer> pick(new Integer(1), new Integer(2));
前文已經(jīng)提到,上面的代碼可以簡寫為:
Integer ret = Demo.pick(new Integer(1), new Integer(2));
因為java編譯器會根據(jù)方法內(nèi)的參數(shù)類型推斷出該方法返回的類型應(yīng)該為Integer,這種機制稱為類型推斷(Type Inference)。
那么問題來了,加入兩個輸入?yún)?shù)為不同的類型,應(yīng)該返回什么類型呢?
例如:
pick("d", new ArrayList<String>());
第一個參數(shù)為String類型,第二個參數(shù)為ArrayList類型,java編譯器就會根據(jù)這兩個參數(shù)類型來推斷,盡量使返回類型為最明確的一種。本例中,String與ArrayList都實現(xiàn)了同樣的接口——Serializable,當(dāng)然,他們也是Object的子類,Serializable類型顯然比Object類型更加明確,因為它的范圍更小更細分,所以最終的返回類型應(yīng)該為Serializable:
Serializable s = pick("d", new ArrayList<String>());
在泛型類實例化的時候同樣可以利用這種機制簡化代碼,需要注意的是,尖括號“<>”在此時是不能省略的。例如:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
//編譯器能推斷出后面的類型,所以可以簡化為:
Map<String, List<String>> myMap = new HashMap<>();
//但是,不能簡化為:
Map<String, List<String>> myMap = new HashMap();
//因為HashMap()是HashMap原始類型(Raw Type)的構(gòu)造函數(shù),而非HashMap<String, List<String>>的構(gòu)造函數(shù),如果不加“<>”編譯器不會進行類型檢查
5、通配符
上文中我們提到過一個例子:
public void boxTest(Box<Number> n){
//……
}
該方法只能接受Box<Number>這一種類型的參數(shù),當(dāng)我們輸入一個Box<Double>或者Box<Integer>時會報錯,盡管Integer與Double是Number的子類??墒侨绻覀兿M摲椒梢越邮躈umber以及它的任何子類,該怎么辦呢?
這時候就要用到通配符了,改寫如下:
public void boxTest(Box<? extends Number> n){
//……
}
? extends Number就代表可以接受Number以及它的子類作為參數(shù)。這種聲明方式被稱為上限通配符(upper bounded wildcard)。
相反地,如果我們希望該方法可以接受Integer,Number以及Object類型的參數(shù)怎么辦呢?應(yīng)該使用下限通配符(lower bounded wildcard):
public void boxTest(Box<? super Integer> n){
//……
}
? super Integer代表可以接受Integer以及它的父類作為參數(shù)。
如果類型參數(shù)中既沒有extends 關(guān)鍵字,也沒有super關(guān)鍵字,只有一個?,代表無限定通配符(Unbounded Wildcards)。
通常在兩種情況下會使用無限定通配符:
- 如果正在編寫一個方法,可以使用Object類中提供的功能來實現(xiàn)
- 代碼實現(xiàn)的功能與類型參數(shù)無關(guān),比如
List.clear()與List.size()方法,還有經(jīng)常使用的Class<?>方法,其實現(xiàn)的功能都與類型參數(shù)無關(guān)。
來看一個例子:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
該方法只能接受List<Object>型的參數(shù),不接受其他任何類型的參數(shù)。但是,該方法實現(xiàn)的功能與List之中參數(shù)類型沒有關(guān)系,所以我們希望它可以接受包含任何類型的List參數(shù)。代碼改動如下:
public static void printList(List<?> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
需要特別注意的是,List<?>與List<Object>并不相同,無論A是什么類型,List<A>是List<?>的子類,但是,List<A>不是List<Object>的子類。
例如:
List<Number> lb = new ArrayList<>();
List<Integer> la = lb; // 會報編譯錯誤,盡管Integer是Number的子類,但是List<Integer>不是List<Number>的子類
List<Integer>與List<Number>的關(guān)系如下:

所以,下面的代碼是正確的:
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // 不會報錯, List<? extends Integer> 是 List<? extends Number>的子類
下面這張圖介紹了上限通配符、下限通配符、無限定通配符之間的關(guān)系:

編譯器可以通過類型推斷機制來決定通配符的類型,這種情況被稱為通配符捕獲。大多時候我們不必擔(dān)心通配符捕獲,除非編譯器報出了包含“capture of”的錯誤。例如:
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0)); //會報編譯錯誤
}
}
上例中,調(diào)用List.set(int,E)方法的時候,編譯器無法推斷i.get(0)是什么類型,就會報錯。
我們可以借助一個私有的可以捕獲通配符的helper方法來解決這種錯誤:
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// 該方法可以確保編譯器通過通配符捕獲來推斷出參數(shù)類型
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
按照約定俗成的習(xí)慣,helper方法的命名方法為“原始方法”+“helper”,上例中,原始方法為“foo”,所以命名為“fooHelper”。
關(guān)于什么時候該使用上限通配符,什么時候該使用下限通配符,應(yīng)該遵循一下幾項指導(dǎo)規(guī)則。
首先將變量分為in-變量與out-變量:in-變量持有為當(dāng)前代碼服務(wù)的數(shù)據(jù),out-變量持有其他地方需要使用的數(shù)據(jù)。
例如copy(src, dest)方法實現(xiàn)了從src源頭將數(shù)據(jù)復(fù)制到dest目的地的功能,那么src就是in-變量,而dest就是out-變量。當(dāng)然,在一些情況下,一個變量可能既是in-變量也是out-變量。
- in-變量使用上限通配符;
- out-變量使用下限通配符;
- 當(dāng)in-變量可以被Object類中的方法訪問時,使用無限定通配符;
- 一個變量既是in-變量也是out-變量時,不使用通配符
注意,上面的規(guī)則不適用于方法的返回類型。
6、類型擦除
java編譯器在處理泛型的時候,會做下面幾件事:
- 將沒有限定的類型參數(shù)用Object替換,保證class文件中只含有正常的類、接口與方法;
- 在必要的時候進行類型轉(zhuǎn)換,保證類型安全;
- 在泛型的繼承上使用橋接方法(bridge methods)保持多態(tài)性。
這類操作被稱為類型擦除(Type Erasure)。
例如:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() {
return data;
}
// ...
}
該類中的T沒有被extends或者super限定,會被編譯器替換成Object:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
// ...
}
如果T加了限定,編譯器會將它替換成合適的類型:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
改造成:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() {
return data;
}
// ...
}
方法中的類型擦除與之類似。
有時候類型擦除會產(chǎn)生一些我們預(yù)想不到的情況,下面通過一個例子來分析它是如何產(chǎn)生的。
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);
}
}
上面的代碼定義了兩個類,MyNode類繼承了Node類,然后運行下面的代碼:
MyNode mn = new MyNode(5);
Node n = mn;
n.setData("Hello");
Integer x = mn.data; // 拋出ClassCastException異常
上面的代碼在類型擦除之后會轉(zhuǎn)換成下面的形式:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn;
n.setData("Hello");
Integer x = (String)mn.data; // 拋出ClassCastException異常
我們來看看代碼是怎么執(zhí)行的:
- (1)
n.setData("Hello")調(diào)用的其實是MyNode類的setData(Object)方法(從Node類繼承的); - (2)n引用的對象中的data字段被賦值一個String變量;
- (3)mn引用的相同對象中的data預(yù)期為Integer類型(mn為Node<Integer>類型);
- (4)第四行代碼試圖將一個String賦值給Integer類型的變量,所以引發(fā)了ClassCastException異常。
當(dāng)編譯一個繼承了帶有參數(shù)化泛型的類或借口時,編譯器會根據(jù)需要創(chuàng)建被稱為bridge method的橋接方法,這是類型擦除中的一部分。
上例中MyNode繼承了Node<Integer>類,類型擦除之后,代碼變?yōu)椋?/p>
class MyNode extends Node {
//編譯器添加的橋接方法
public void setData(Object data) {
setData((Integer) data);
}
// MyNode的該方法并沒有覆寫父類的setData(Object data)方法,因為參數(shù)類型不一樣
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
7、注意事項
為了高效地使用泛型,應(yīng)該注意下面幾個方面:
(1)不能用基本類型實例化類型參數(shù)
例如
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// ...
}
當(dāng)創(chuàng)建一個Pair類時,不能用基本類型來替代K,V兩個類型參數(shù)。
Pair<int, char> p = new Pair<>(8, 'a'); // 編譯錯誤
Pair<Integer, Character> p = new Pair<>(8, 'a'); //正確寫法
(2)不可實例化類型參數(shù)
例如:
public static <E> void append(List<E> list) {
E elem = new E(); // 編譯錯誤
list.add(elem);
}
但是,我們可以通過反射實例化帶有類型參數(shù)的對象:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // 正確
list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class); //傳入類型參數(shù)的Class對象
(3)不能在靜態(tài)字段上使用泛型
通過一個反例來說明:
public class MobileDevice<T> {
private static T os; //假如我們定義了一個帶泛型的靜態(tài)字段
// ...
}
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
因為靜態(tài)變量是類變量,被所有實例共享,此時,靜態(tài)變量os的真實類型是什么呢?顯然不能同時是Smartphone、Pager、TabletPC。
這就是為什么不能在靜態(tài)字段上使用泛型的原因。
(4)不能對帶有參數(shù)化類型的類使用cast或instanceof方法
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 編譯錯誤
// ...
}
}
傳給蓋該方法的參數(shù)化類型集合為:
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
運行環(huán)境并不會跟蹤類型參數(shù),所以分辨不出ArrayList<Integer>與ArrayList<String>,我們能做的至多是使用無限定通配符來驗證list是否為ArrayList:
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // 正確
// ...
}
}
同樣,不能將參數(shù)轉(zhuǎn)換成一個帶參數(shù)化類型的對象,除非它的參數(shù)化類型為無限定通配符(<?>):
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 編譯錯誤
當(dāng)然,如果編譯器知道參數(shù)化類型肯定有效,是允許這種轉(zhuǎn)換的:
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // 允許轉(zhuǎn)變,類型參數(shù)沒變化
(5)不能創(chuàng)建帶有參數(shù)化類型的數(shù)組
例如:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 編譯錯誤
下面通過兩段代碼來解釋為什么不行。先來看一個正常的操作:
Object[] strings = new String[2];
strings[0] = "hi"; // 插入正常
strings[1] = 100; //報錯,因為100不是String類型
同樣的操作,如果使用的是泛型數(shù)組,就會出問題:
Object[] stringLists = new List<String>[]; // 該句代碼實際上會報錯,但是我們先假定它可以執(zhí)行
stringLists[0] = new ArrayList<String>(); // 插入正常
stringLists[1] = new ArrayList<Integer>(); // 該句代碼應(yīng)該報ArrayStoreException的異常,但是運行環(huán)境探測不到
(6)不能創(chuàng)建、捕獲泛型異常
泛型類不能直接或間接繼承Throwable類
class MathException<T> extends Exception { /* ... */ } //編譯錯誤
class QueueFullException<T> extends Throwable { /* ... */} // 編譯錯誤
方法不能捕獲泛型異常:
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // 編譯錯誤
// ...
}
}
但是,我們可以在throw子句中使用類型參數(shù):
class Parser<T extends Exception> {
public void parse(File file) throws T { // 正確
// ...
}
}
(7)不能重載經(jīng)過類型擦除后形參轉(zhuǎn)化為相同原始類型的方法
先來看一段代碼:
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印結(jié)果可能與我們猜測的不一樣,打印出的是true,而非false,因為一個泛型類的所有實例在運行時具有相同的運行時類(class),而不管他們的實際類型參數(shù)。
事實上,泛型之所以叫泛型,就是因為它對所有其可能的類型參數(shù),有同樣的行為;同樣的類可以被當(dāng)作許多不同的類型。
認識到了這一點,再來看下面的例子:
public class Example {
public void print(Set<String> strSet) { } //編譯錯誤
public void print(Set<Integer> intSet) { } //編譯錯誤
}
因為Set<String>與Set<Integer>本質(zhì)上屬于同一個運行時類,在經(jīng)過類型擦出以后,上面的兩個方法會共享一個方法簽名,相當(dāng)于一個方法,所以重載出錯。