一、引入泛型機制的原因
假如我們想要實現一個String數組,并且要求它可以動態(tài)改變大小,這時我們都會想到用ArrayList來聚合String對象。然而,過了一陣,我們想要實現一個大小可以改變的Date對象數組,這時我們當然希望能夠重用之前寫過的那個針對String對象的ArrayList實現。
在Java 5之前,ArrayList的實現大致如下:
public class ArrayList {
public Object get(int i) { ... }
public void add(Object o) { ... }
...
private Object[] elementData;
}
基于繼承的泛型實現會帶來兩個問題:第一個問題是有關get方法的,我們每次調用get方法都會返回一個Object對象,每一次都要強制類型轉換為我們需要的類型,這樣會顯得很麻煩;第二個問題是有關add方法的,假如我們往聚合了String對象的ArrayList中加入一個File對象,編譯器不會產生任何錯誤提示,而這不是我們想要的。
所以,從Java 5開始,ArrayList在使用時可以加上一個類型參數(type parameter),這個類型參數用來指明ArrayList中的元素類型。類型參數的引入解決了以上提到的兩個問題,如以下代碼所示:
ArrayList<String> s = new ArrayList<String>();
s.add("abc");
String s = s.get(0); //無需進行強制轉換
s.add(123); //編譯錯誤,只能向其中添加String對象
...
在以上代碼中,編譯器“獲知”ArrayList的類型參數String后,便會替我們完成強制類型轉換以及類型檢查的工作。
二、泛型類
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
這樣我們的Box類便可以得到復用,我們可以將T替換成任何我們想要的類型:
Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
三、泛型方法
聲明一個泛型方法很簡單,只要在返回類型前面加上一個類似<K, V>的形式就行了:
public class Util {
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());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
我們可以像下面這樣去調用泛型方法:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
或者在Java1.7/1.8利用type inference,讓Java自動推導出相應的類型參數:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
四、邊界符
現在我們要實現這樣一個功能,查找一個泛型數組中大于某個特定元素的個數,我們可以這樣實現:
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
但是這樣很明顯是錯誤的,因為除了short, int, double, long, float, byte, char等原始類型,其他的類并不一定能使用操作符>,所以編譯器報錯,那怎么解決這個問題呢?答案是使用邊界符。
public interface Comparable<T> {
public int compareTo(T o);
}
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
五、通配符(PECS原則)
1 <? extends T>
首先我們先定義幾個簡單的類,下面我們將用到它:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
下面這個例子中,我們創(chuàng)建了一個泛型類Reader,然后在f1()中當我們嘗試Fruit f = fruitReader.readExact(apples);編譯器會報錯,因為List<Fruit>與List<Apple>之間并沒有任何的關系。
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f1() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
// Errors: List<Fruit> cannot be applied to List<Apple>.
// Fruit f = fruitReader.readExact(apples);
}
public static void main(String[] args) {
f1();
}
}
但是按照我們通常的思維習慣,Apple和Fruit之間肯定是存在聯系,然而編譯器卻無法識別,那怎么在泛型代碼中解決這個問題呢?我們可以通過使用通配符來解決這個問題:
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f2() {
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f2();
}
這樣就相當與告訴編譯器, fruitReader的readCovariant方法接受的參數只要是滿足Fruit的子類就行(包括Fruit自身),這樣子類和父類之間的關系也就關聯上了。
2 <? super T>
上面我們看到了類似<? extends T>的用法,利用它我們可以從list里面get元素,那么我們可不可以往list里面add元素呢?我們來嘗試一下:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple())
// flist.add(new Orange())
// flist.add(new Fruit())
// flist.add(new Object())
flist.add(null); // Legal but uninteresting
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
答案是否定,Java編譯器不允許我們這樣做,為什么呢?對于這個問題我們不妨從編譯器的角度去考慮。因為List<? extends Fruit> flist它自身可以有多種含義:
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
- 當我們嘗試add一個Apple的時候,flist可能指向new ArrayList<Orange>();
- 當我們嘗試add一個Orange的時候,flist可能指向new ArrayList<Apple>();
- 當我們嘗試add一個Fruit的時候,這個Fruit可以是任何類型的Fruit,而flist可能只想某種特定類型的Fruit,編譯器無法識別所以會報錯。
所以對于實現了<? extends T>的集合類只能將它視為Producer向外提供(get)元素,而不能作為Consumer來對外獲取(add)元素。
如果我們要add元素應該怎么做呢?可以使用<? super T>:
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static void f1() {
writeExact(apples, new Apple());
writeExact(fruit, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item)
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) {
f1(); f2();
}
}
根據上面的例子,我們可以總結出一條規(guī)律,”Producer Extends, Consumer Super”:
- “Producer Extends” – 如果你需要一個只讀List,用它來produce T,那么使用? extends T。
- “Consumer Super” – 如果你需要一個只寫List,用它來consume T,那么使用? super T。
如果需要同時讀取以及寫入,那么我們就不能使用通配符了。
如何閱讀過一些Java集合類的源碼,可以發(fā)現通常我們會將兩者結合起來一起用,比如像下面這樣:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
六、類型擦除
類型擦除就是說Java泛型只能用于在編譯期間的靜態(tài)類型檢查,然后編譯器生成的代碼會擦除相應的類型信息,這樣到了運行期間實際上JVM根本就不知道泛型所代表的具體類型。這樣做的目的是因為Java泛型是1.5之后才被引入的,為了保持向下的兼容性,所以只能做類型擦除來兼容以前的非泛型代碼。對于這一點,如果閱讀Java集合框架的源碼,可以發(fā)現有些類其實并不支持泛型。
我們先來看一下下面這個簡單的例子:
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; }
// ...
}
編譯器做完相應的類型檢查之后,實際上到了運行期間上面這段代碼實際上將轉換成:
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; }
// ...
}
由于在虛擬機中泛型類Pair變?yōu)樗膔aw type,因而getData方法返回的是一個Object對象,而從編譯器的角度看,這個方法返回的是我們實例化類時指定的類型參數的對象。實際上,是編譯器幫我們完成了強制類型轉換的工作。也就是說編譯器會把對Node泛型類中getData方法的調用轉化為兩條虛擬機指令:
- 第一條是對raw type方法getFirst的調用,這個方法返回一個Object對象;
- 第二條指令把返回的Object對象強制類型轉換為當初我們指定的類型參數類型。
七、泛型注意事項
1 不能用基本類型實例化類型參數
也就是說,以下語句是非法的:
Pair<int, int> pair = new Pair<int, int>();
2 不能拋出也不能捕獲泛型類實例
泛型類擴展Throwable即為不合法,因此無法拋出或捕獲泛型類實例。但在異常聲明中使用類型參數是合法的:
public static <T extends Throwable> void doWork(T t) throws T {
try {
...
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
3 參數化類型的數組不合法
在虛擬機進行類型擦除后,實際上pairs成為了Pair[]數組,我們可以將它向上轉型為Object[]數組。這時我們若往其中添加Pair<Date, Date>對象,便能通過編譯時檢查和運行時檢查,而我們的本意是只想讓這個數組存儲Pair<String, String>對象,這會產生難以定位的錯誤。因此,Java不允許我們通過以上的語句形式聲明并初始化一個泛型數組。
Pair<String, String>[] pairs = new Pair<String, String>[10];
可用如下語句聲明并初始化一個泛型數組:
Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[10];
4 不能實例化類型變量
不能以諸如“new T(...)", "new T[...]", "T.class"的形式使用類型變量。Java禁止我們這樣做的原因很簡單,因為存在類型擦除,所以類似于"new T(...)"這樣的語句就會變?yōu)椤眓ew Object(...)", 而這通常不是我們的本意。我們可以用如下語句代替對“new T[...]"的調用:
arrays = (T[]) new Object[N];
5 泛型類的靜態(tài)上下文中不能使用類型變量
因為普通類中可以定義靜態(tài)泛型方法,關于為什么有這樣的規(guī)定,請考慮下面的代碼:
public class People<T> {
public static T name;
public static T getName() {
...
}
}
我們知道,在同一時刻,內存中可能存在不只一個People<T>類實例。假設現在內存中存在著一個People<String>對象和People<Integer>對象,而類的靜態(tài)變量與靜態(tài)方法是所有類實例共享的。那么問題來了,name究竟是String類型還是Integer類型呢?基于這個原因,Java中不允許在泛型類的靜態(tài)上下文中使用類型變量。