簡介
JDK 5.0以后增加了幾個新的擴展功能,其中一個就是泛型。泛型允許我們把類型泛化。以5.0版本以前的Collections為例子:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
比如說第三行,程序員實際上是知道數(shù)據(jù)是什么類型,但不得不進行強制轉化,因為編譯器只能保證從iterator返回的類型是Object。當然有時候程序員也會犯錯,比如說記錯返回類型導致運行時錯誤。
為了避免上述問題,引入泛型,改造如下
List<Integer> myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
通過傳入類型參數(shù)Integer,在第三步把煩人的強制轉化給去掉了,編譯器現(xiàn)在在編譯階段確保返回的類型安全,避免了上述問題,這也增強了程序的可讀性及穩(wěn)定性。
定義基本泛型
拿List作為例子
public interfacep List <E> {
void add(E x);
Iterator<E> iterator();
}
在簡介中我們定義了List<Integer>,我們或者可以認為List<Integer>是E被Integer代表的List的版本,類似下面的代碼:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
但實際上在Java中,源文件、可執(zhí)行文件、硬盤已經(jīng)內(nèi)存中都不會存在多份不同類型的List實現(xiàn),這跟C++的模板實現(xiàn)方式很不一樣。泛化類只會編譯一次,生成一個class文件,跟原來的類或者接口生成一樣。
泛型及子類型
比如下面的代碼
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第一步明顯不會有問題,第二步是不是合法呢,也就是說List<String>是不是List<Object>的子類。為了確認一下,我們增加多幾行代碼
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 試圖把Object強制轉化成String
假設第二步合法,則我們可以在lo中插入任意對象。因此ls可能會包含非String對象,從而有可能導致出錯。
實際上第2步代碼,編譯器在編譯階段就會報編譯錯誤,因為List<String>不是List<Object>的子類。
通配符
假設需要為collection寫一個打印所有元素的方法,在5.0版本以前可以這樣寫
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
5.0版本以后嘗試寫一個新的通用版本,比如下面的代碼
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
實際上這個新版本會不如老的那個版本。老的版本可以支持任意的collection類型,而新版本只支持Collection<Object>,因為Collection<Object>并不是我們想要任意collection類型的父類。
那什么才是任意collection的父類呢?Collection<?>,?就是所說的通配符,表示未知類型的collection,可以匹配任意的collection。代碼改寫如下
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
上面代碼中我們讀取出來的元素賦予Object類型,這樣做是安全的,因為無論是哪類型的collection,它里面的元素都可以轉成Object。但是,向里面添加任意對象確是不安全的,例如
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 編譯錯誤
由于?代表未知類型,所以不能把Object添加進去。
帶限定的通配符
比如說以下這段代碼
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
每個畫布上實際上有一系列形狀要畫,假設我們添加一個drawAll的函數(shù)
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
很明顯上述只能支持List<Shape>的繪制,與我們的預期不符合。通過帶限定的通配符,可以改成
public void drawAll(List<? extends Shape> shapes) {
...
}
通過這種方式,告訴編譯器,?號代表的未知類型實際上是Shape的自身或者子類。
泛型方法
假設我們需要寫一個方法,把Object數(shù)組的元素全部放入Collection中,假設代碼如下
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // 編譯錯誤
}
}
當然通過上述章節(jié),可以把Collection<?>改成Collection<Object>,但這并非是我們想要的。通過泛型方式改造
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // 正確
}
}
上面會涉及到一個問題,什么時候使用通配符,什么時候需要使用泛型方法。為了了解,我們看一下Collection內(nèi)部的幾個方法
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我們可以用泛型方法重寫
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
}
可以看到,在兩個方法中,T實際上只是被用到1次,同時返回值并不依賴于T。而在上述例子中只是為了支持不同類型的Collection可以被傳入而已,返回值也跟傳入的類型參數(shù)無關,使用通配符更加適合。
關于泛型的Class
假設有以下這段代碼
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
第3步返回的結果實際上是true,因為無論泛化類型是否一樣,同樣泛型類的實體對象在運行時的Class都一樣。
的確,事實上一個泛型化的類是指該類支持無論傳入任何的泛化類型都遵守同樣的行為。因此,泛化參數(shù)和泛化修飾符無法作用于靜態(tài)方法或者靜態(tài)區(qū)域,因為靜態(tài)方法類的所有的實例都會公用,違背上述原則。
基于上面的描述,假設以下的代碼
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ... }
第2步實際上非法,因為所有的泛型類實例共享同一個泛型類,并不存在Collection<String>。同樣的,如下面的代碼
Collection<String> cstr = (Collection<String>) cs; //1
<T> T badCast(T t, Object o) {
return (T) o; // 2
}
第1段代碼會拋出Unchecked warning,因為Collection<String>在運行時并不存在,所以運行時系統(tǒng)實際上并不會做檢查。同樣,第3段代碼中T在運行時也不存在。這表示使用過程中我們并不能保證代碼安全。