編者注:在從Java in a Nutshell,5th edith中摘錄的兩部部分中的
第一部分,David Flanagan描述了如何使用泛型。這部分,David Flanagan將具體告訴你如何創建自己的泛型和泛型方法,并且以Java核心API很多重要的泛型作為結束總結。
創建泛型和泛型方法
創建一個簡單的泛型是非常容易的。首先,在一對尖括號(< >)中聲明類型變量,以逗號間隔變量名列表。在類的實例變量和方法中,可以在任何類型的地方使用那些類型變量。切記,類型變量僅在編譯時存在,所以不能使用instanceof和new這類運行時操作符來操作類型變量。
讓我們以一個簡單的例子來開始這部分的學習,而后將精簡這個例子。這段代碼定義了一個樹形數據結構,使用類型變量V代表存儲在各個樹結點中的值。
import java.util.*;
/**
* A tree is a data structure that holds values of type V.
* Each tree has a single value of type V and can have any number of
* branches, each of which is itself a Tree.
*/
public class Tree {
// The value of the tree is of type V.
V value;
// A Tree can have branches, each of which is also a Tree
List<tree branches = new ArrayList<tree();
// Here's the constructor. Note the use of the type variable V.
public Tree(V value) { this.value = value; }
// These are instance methods for manipulating the node value and branches.
// Note the use of the type variable V in the arguments or return types.
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree getBranch(int n) { return branches.get(n); }
void addBranch(Tree branch) { branches.add(branch); }
}
正如你所看到的,命名一個類型變量習慣于一個大寫字母。使用一個字母可以同現實中那些具有描述性的,長的實際變量名有所區別。使用大寫字母要同變量命名規則一致,并且要區別于局部變量,方法參數,成員變量,而這些變量常常使用一個小寫字母。集合類中,比如java.util中常常使用類型變量E代表“Element type”。T和S常常用來表示范型變量名(好像使用i和j作為循環變量一樣)。
注意到,當一個變量被聲明為泛型時,只能被實例變量和方法調用(還有內嵌類型)而不能被靜態變量和方法調用。原因很簡單,參數化的泛型是一些實例。靜態成員是被類的實例和參數化的類所共享的,所以靜態成員不應該有類型參數和他們關聯。方法,包括靜態方法,可以聲明和使用他們自己的類型參數,但是,調用這樣一個方法,可以被不同地參數化。這些內容將在本章后面談到。
類型變量綁定
上面例子中的Tree
中的類型變量V是不受約束的:Tree可以被參數化為任何類型。以前我們常常會設置一些約束條件在需要使用的類型上:也許我們需要強制一個類型參數實現一個或多個接口,或是一個特定類的子類。這可以通過指明類型綁定來完成。我們已經看到了統配符的上界,而且使用簡單的語法可以指定一般類型變量的上界。后面的代碼,還是使用Tree這個例子,并且通過實現Serializable和Comparable來重寫。為了做到這點,例子中使用類型變量綁定來確保值類型的Serializable和Comparable。
import java.io.Serializable;
import java.util.*;
public class Tree>
implements Serializable, Comparable<tree
{
V value;
List<tree branches = new ArrayList<tree();
public Tree(V value) { this.value = value; }
// Instance methods
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree getBranch(int n) { return branches.get(n); }
void addBranch(Tree branch) { branches.add(branch); }
// This method is a nonrecursive implementation of Comparable<tree
// It only compares the value of this node and ignores branches.
public int compareTo(Tree that) {
if (this.value == null && that.value == null) return 0;
if (this.value == null) return -1;
if (that.value == null) return 1;
return this.value.compareTo(that.value);
}
// javac -Xlint warns us if we omit this field in a Serializable class
private static final long serialVersionUID = 833546143621133467L;
}
一個類型變量的綁定是通過extends后的名字和一個類型列表(這可以是參數化的,就像Comparable一樣)表達的。注意當有不止一個綁定時,就像上面例子中的,綁定的類型要用&作為分隔符,而不是使用逗號。都后用來分隔類型變量,如果用來分隔類型變量綁定,就會模棱兩可。一個類型變量可以有任何數量的綁定,包括任何數量的借口和至多一個類。
范型中的通配符
上一章的例子中我們看到了通配符和控制參數化類型的通配符綁定。這些在范型中同樣非常有用。當前設計的Tree要求每個節點有相同類型的值,V。也許這樣太嚴格了,也許我們應該讓Tree的branches能夠存放V的子類而不全是V。這個版本的Tree(刪除了Comparable和Serializable接口的實現)這樣做會更靈活。
public class Tree {
// These fields hold the value and the branches
V value;
List<tree<? extends V>> branches = new ArrayList<tree<? extends V>>();
// Here's a constructor
public Tree(V value) { this.value = value; }
// These are instance methods for manipulating value and branches
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree<? extends V> getBranch(int n) { return branches.get(n); }
void addBranch(Tree<? extends V> branch) { branches.add(branch); }
}
通配符綁定允許我們在枝節點上增加一個Tree,比如,一個樹枝Tree:
Tree t = new Tree(0); // Note autoboxing
t.addBranch(new Tree(1)); // int 1 autoboxed to Integer
通過getBranch()查詢樹枝,而樹枝的返回類型不知道,所以必須使用統配符來表達。接下來的兩個是合法的,但第三個不是:
Tree<? extends Number> b = t.getBranch(0);
Tree<?> b2 = t.getBranch(0);
Tree b3 = t.getBranch(0); // compilation error
當我們這樣來查詢一個樹枝時,不能精確確定它的返回類型,但是存在類型的上限,所以,我們可以這樣做:
Tree<? extends Number> b = t.getBranch(0);
Number value = b.getValue();
那我們不能做什么呢?設定樹枝的值,或者在原有的樹枝上添加新的樹枝。早前章節解釋的,上界的存在不會改變返回值的類型不可知,編譯器沒有足夠的信息讓我們安全的給setValue()或者一個樹枝(包括值類型)的addBranch()傳遞一個值。下面的兩行代碼都是非法的:
b.setValue(3.0); // Illegal, value type is unknown
b.addBranch(new Tree(Math.PI));
這個例子在設計時找到了一個平衡點:使用綁定通配符使得數據結構更加靈活,但是減少了安全使用其中方法的可能。這個設計是好是壞就要根據上下文聯系了。通常,好的范型設計是非常困難的。幸運的是,大多我們要使用的已經在java.util包中設計好了,而不用我們自己再去設計。
范型方法
正如前面說的,范型只能被實例成員調用,而不是靜態成員。同實例方法一樣,靜態方法也可以使用通配符。盡管靜態方法不能使用包含他們的類中的類型變量,但是他們可以聲明自己的類型變量。當一個方法聲明了自己的類型變量,就叫做范型方法。
這里有一個要添加到Tree中的靜態方法。他不是一個范型方法,但是使用了綁定的通配符,就好像先前我們看到的sumList()一樣:
/** Recursively compute the sum of the values of all nodes on the tree */
public static double sum(Tree<? extends Number> t) {
double total = t.value.doubleValue();
for(Tree<? extends Number> b : t.branches) total += sum(b);
return total;
}
通過通配符的上界綁定,聲明自己的類型變量來重寫這個方法:
public static double sum(Tree t) {
N value = t.value;
double total = value.doubleValue();
for(Tree<? extends N> b : t.branches) total += sum(b);
return total;
}
范型的sum()不比通配符版本的簡單,而且聲明變量并沒有讓我們獲得什么。這種情況下,通配符方案要比范型方法更有效,當一個類型變量用來表達兩個參數之間或者參數和返回值之間的關系時,范型方法才是需要的。請看下面的例子:
// This method returns the largest of two trees, where tree size
// is computed by the sum() method. The type variable ensures that
// both trees have the same value type and that both can be passed to sum().
public static Tree max(Tree t, Tree u) {
double ts = sum(t);
double us = sum(u);
if (ts > us) return t;
else return u;
}
這個方法使用類型變量N來約束參數和返回值有相同類型,并且參數是Number或者他的子類。
使得參數具有相同類型也許是有爭議的,應該讓我們能調用max()不論是Tree或者Tree。一種方法是使用兩個不相干的類型變量來表示兩個不相干的值類型。注意,我們不能在方法的返回時使用變量而必須使用通配符:
public static
Tree<? extends Number> max(Tree t, Tree u) {...}
既然兩個類型變量N和M沒有任何聯系,而且每個僅在簽名的時候使用,他們沒有提供比通配符更多的好處,這種方法最好這樣寫:
public static Tree<? extends Number> max(Tree<? extends Number> t,
Tree<? extends Number> u) {...}
所有在這里的范型方法都是靜態的,這并不是必須的,實例方法也可以聲明自己的類型變量。
調用范型方法
當你使用范型時,必須指定實際類型參數來代替相應的類型變量。但這些對范型方法有些不同:編譯器總是能計算出基于你所傳遞的參數的相應范型方法參數。考慮一下上面定義的max(),作為例子:
public static Tree max(Tree t, Tree u) {...}
當你調用這個方法時,不需要指明N,因為N是隱含地由t和u指明。在后面的代碼中,編譯器決定N為Integer:
Tree x = new Tree(1);
Tree y = new Tree(2);
Tree z = Tree.max(x, y);
編譯器判斷范型方法的參數類型稱為類型推斷。類型推斷是相對于知覺推斷的。而實際編譯器的實現方法是一種非常復雜的過程,超過了這本書的討論范圍。更多的細節在The Java Language Specification, Third Edition的第十五章。
讓我們看一個更加復雜的類型推斷,考慮一下這個方法:
public class Util {
/** Set all elements of a to the value v; return a. */
public static T[] fill(T[] a, T v) {
for(int i = 0; i < a.length; i++) a[i] = v;
return a;
}
}
這里有兩個該方法的調用:
Boolean[] booleans = Util.fill(new Boolean[100], Boolean.TRUE);
Object o = Util.fill(new Number[5], new Integer(42));
在第一個例子中,編譯器可以輕松的推斷出T是Boolean類型,第二個例子中,編譯器判斷T是Number。
在非常罕見的情況下,你可能會顯示的指明范型方法的參數類型。有時候這是必要的,比如,當范型方法不需要參數時。考慮一下java.util.Collections.emptySet():返回一個空集合,但是不同于Collections.singleton()(可以在參考部分察看),他不帶任何參數,但需要指明返回類型。通過在方法名前的<>中,可以顯示的指明參數類型:
Set empty = Collections.emptySet();
類型參數不能同沒有限制的方法名結合使用:他們必須跟隨在一個.后或者在關鍵字new后,或者在關鍵字this前,或者構造函數的super前。
可以證明,如果如果你將Collections.emptySet()的返回值賦給一個變量,就像我們上邊通過類型推斷機制推斷基于變量類型的參數類型。盡管顯示的類型說明可以更加清楚,但這不是必要的,可以像下面一樣重寫:
Set empty = Collections.emptySet();
在方法調用表達式中,顯示的說明emptySet()的返回值類型是必要的。比如,假設你要調用一個名為printWords()的方法,該方法僅需一個Set的參數,如果你想傳遞一個空的集合給該方法,就要像下面一樣寫:
printWords(Collections.emptySet());
這種情況下,顯示的類型說明是必要的。
范型方法和數組
早先我們看到,編譯器不允許創建一個類型參數化的數組。但是對于范型的使用會是不同的。考慮一下前面定義的Util.fill(),它得以第一個參數和返回值類型都是T[]。而方法體內不必創建任何參數為T的數組,所以這個方法是合法的。
如果你創建一個方法使用varargs(參見第二章的2.6.4)和類型變量,記住調用varargs隱含創建一個數組,請看下面的例子:
/** Return the largest of the specified values or null if there are none */
public static > T max(T... values) { ... }
你可以使用一個Integer類型來調用這個方法,因為編譯器會在調用的時候插入必要的數組創建代碼。但是你不能將參數轉換為Comparable來調用這個方法,因為創建一個Comparable[]是不合法的。
參數化異常
異常是在運行時拋出和捕獲的。沒有辦法讓編譯器完成類型檢查,來保證在catch塊中拋出的未知的類型匹配異常。由于這個原因,catch塊很可能不包含類型變量和通配符。既然不可能保證在運行時捕獲一個編譯器時類型參數完整性異常,所以不允許創建任何Throwable類型的子類。參數化異常是不允許的。
但是你可以使用類型變量在throw塊里的方法簽名中。看看下面的例子:
public interface Command {
public void doit(String arg) throws X;
}
這個接口描述了一個“command”:一塊代碼只有一個String類型的參數,沒有返回值。代碼可能拋出一個類型為X的異常。這里有一個例子使用這個接口:
Command save = new Command() {
public void doit(String filename) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter(filename));
out.println("hello world");
out.close();
}
};
try { save.doit("/tmp/foo"); }
catch(IOException e) { System.out.println(e); }
范型個案研究:比較和枚舉
Java1.5引入的范型新特性,在1.5的API中有使用,特別多的是在java.util包中,但是在java.lang,java.lang.reflect和java.util.concurrent中也有。這些API都是經過仔細的斟酌創建的,通過學習這些API我們可以學到很多好的設計方法。
java.util中的范形是比較簡單的:因為大多都是集合類,類型變量也是代表集合中的元素。java.lang中的幾個重要范型是比較難以理解的,他們不是集合,而且第一眼很不容易理解為什么設計成范型。學習這些范型可以讓我們更深層次的理解范形的工作機制,并且介紹一些我們沒有提到的概念。特別的,我們要檢查Comparable接口和Enum類(枚舉類型的超類,后面一張講解)并且學習一些重要但是很少使用的范型特性,比如通配符下界。
在java1.5中,Comparable接口被修改為范型的。大多數的類都實現了這個接口,考慮一下Integer:
public final class Integer extends Number implements Comparable
原先的Comparable接口在類型安全方面是有問題的。兩個繼承了Comparable接口的對象可能不能相互比較。JDK5.0前,非范形的Comparable接口是非常有用但是不安全的,而現在的接口,捕獲了我們需要的信息:他告訴我們一個對象是可比較的,并且可以同什么比較。
現在,考慮一下comparable類的子類。Integer是final的,所以不能有子類,那么讓我們看看java.math.BigInteger:
public class BigInteger extends Number implements Comparable
如果我們實現一個BiggerInteger類是BigInteger的子類,他從父類那里繼承了Comparable接口,但是注意繼承的是Comparable而不是Comparable。這意味著BigInteger和BiggerInteger是可以相互比較的,這是非常好的。BiggerInteger可以重載compareTo(),但是不允許實現一個不同的參數化的Comparable。這就是說BiggerInteger不能同時繼承BigInteger和實現Comparable。
當你使用可比較的對象時(當寫排序算法的時候)記住兩點。首先,使用原生類型是不夠充分的:考慮到類型安全,必須指明同什么比較。接下來,類型是不允許同自己比較的:有時候他會同他的祖先比較。為了具體說明,考慮java.util.Collections.max():
這是一個冗長而且復雜的方法標簽,我們來一步步考慮:
方法中包含一個類型變量T,并且有復雜的綁定,稍后我們返回來討論。
方法的返回值類型是T。
方法名是max()。
方法的參數是一個集合。元素的類型指定為綁定的通配符。我們并不知道元素的確切類型,但直到有一個上限T。所以我們知道元素的類型要么為T,要么是T的子類。集合的任何元素都可以作為返回值使用。
這些是比較簡單的,本章我們已經看到了通配符上界,我們再來看看max()中的類型變量聲明:
>
要說明的第一點,T必須實現了Comparable接口。(范型的語法使用關鍵字extends來代表類型綁定,不論是類或接口)這是期望的,因為這個方法是找到集合中最大的元素。但是觀察這個參數化的Comparable接口,這是一個通配符,但是這個通過關鍵字super來綁定,而不是extends。這是下界綁定。? extends T是我們熟悉的上界綁定:這意味著T或者其子類。? super T比較少用:這意味著T或者他的超類。
總結一下,類型變量聲明表明:“T是一個實現了Comparable接口或者他的父類實現了該接口的類型。”Collections.min()和Collections.binarySearch()有著相同的聲明。
對其他的下界通配符(對于Comparable接口沒有作用)的例子,Collections中的addAll(),copy(),和fill()。觀察addAll()的聲明:
public static boolean addAll(Collection<? super T> c, T... a)
這是一個varargs方法,接受任意數量的參數,并且傳遞給他們一個T[],命名為a。他將a中的所有元素都賦給集合c。集合的元素類型雖然不知道,但是有一個下界:元素均為T或者T的超類。不論類型是什么,我們可以確定數組的元素都是類型的實例,所以將數組的元素添加到集合中是合法的。
返回到我們先前討論的上界通配符,如果有一個集合的元素是上界通配符,那么都是只讀的。考慮List<? extends Serializable>。我們知道,所有的元素都是Serializable,所以像get()這樣的方法返回一個Serializable類型的返回值。編譯器不允許我們調用add()這樣的方法,因為實際的元素類型是不可知的。不能夠添加絕對的Serializable對象到list中,因為實現他們的類可能不是正確的類型。
既然上界統配符的結果是只讀的,所以你可能會期望下界通配符來實現只寫的集合。實際并不是這樣,假設這里有一個List<? extends Integer>。元素的實際類型是不知道的,但是可能性是Integer或者他的祖先類Number和Object。無論實際類型是什么,將Integer類型(而不是Number和Object對象)的元素添加到list中是安全的。無論實際類型是什么,list中所有元素都是Object對象的實例,所以list中像get()一樣的方法返回Object。
最后,讓我們把注意力放到java.lang.Enum類。Enum是所有枚舉類型的父類,它實現了Comparable接口,但是有一個讓人迷惑的范型聲明方法:
public class Enum> implements Comparable, Serializable
第一眼,類型變量E的聲明在一個循環中。再仔細的看一看:聲明真正說明了,Enum必須是一個本身就是Enum類型的類型。這種表面上的循環是很顯然的,如果我們看到了implements子句。正如我們看到的,Comparable類通常被定義為可以同自己比較的。而且他們的子類也可以同他們的父類比較。從另一個方面將,Enum實現了Comparable接口不是為了他本身,而是為了他的子類E。
資源:
·Onjava.com:Onjava.com
·Matrix-Java開發者社區:http://www.matrix.org.cn/
Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
Java中的泛型 第二部分
泛型類型,第一部分
作者: David Flanagan
翻譯:cat
版權聲明:可以任意轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
作者:
David Flanagan;
cat
原文地址:
http://www.onjava.com/pub/a/onjava/excerpt/javaian5_chap04/index.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43864_Generic_Types.html
關鍵詞: Generic Types
編輯按:《Java in a Nutshell, 5th Edition》覆蓋了jdk5.0中很多變化和新特征,其中最重要的就是泛型。在本文的第一部分,作者David Flanagan介紹了如何使用泛型;而在第二部分,作者描述了如何寫你自己的泛型和泛型方法。
Java5.0的新特性之一是引入了泛型類型和泛型方法。一個泛型類型通過使用一個或多個類型變量來定義,并擁有一個或多個使用一個類型變量作為一個參數或者返回值的占位符。例如,類型java.util.List<E>是一個泛型類型:一個list,其元素的類型被占位符E描述。這個類型有一個名為add()的方法,被聲明為有一個類型為E的參數,同時,有一個get()方法,返回值被聲明為E類型。
為了使用泛型類型,你應該為類型變量詳細指明實際的類型,形成一個就像List<String>類似的參數化類型。[1]指明這些額外的類型信息的原因是編譯器據此能夠在編譯期為您提供很強的類型檢查,增強您的程序的類型安全性。舉個例子來說,您有一個只能保持String對象的List,那么這種類型檢查就能夠阻止您往里面加入String[]對象。同樣的,增加的類型信息使編譯器能夠為您做一些類型轉換的事情。比如,編譯器知道了一個List<String>有個get()方法,其返回值是一個String對象,因此您不再需要去將返回值由一個Object強制轉換為String。
Java.util包中的集合類在java5.0中已經被做成了泛型,也許您將會在您的程序中頻繁的使用到他們。類型安全的集合類就是一個泛型類型的典型案例。即便您從沒有定義過您自己的泛型類型甚至從未用過除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個標志——他們證明了這個主要的新語言特性的復雜性。
我們從探索類型安全的集合類中的基本的泛型用法開始,進而研究更多使用泛型類型的復雜細節。然后我們討論類型參數通配符和有界通配符。描繪了如何使用泛型以后,我們闡明如何編寫自己的泛型類型和泛型方法。我們對于泛型的討論將結束于一趟對于JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對泛型如何工作這個問題有個深入的理解。
類型安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對象的set、對象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數來界定集合中的對象的類型。這個討論并不適合java1.4或更早期版本。如果沒有泛型,對于集合類的使用需要程序員記住每個集合中元素的類型。當您在java1.4種創建了一個集合,您知道您放入到集合中的對象的類型,但是編譯器不知道。您必須小心地往其中加入一個合適類型的元素,當需要從集合中獲取元素時,您必須顯式的寫強制類型轉換以將他們從Object轉換為他們真是的類型。考察下邊的java1.4的代碼。
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn't know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn't know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
泛型類型解決了這段代碼中的顯示的類型安全問題。Java.util中的List或是其他集合類已經使用泛型重寫過了。就像前面提到的, List被重新定義為一個list,它中間的元素類型被一個類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個類型為E的參數,用于替換以前的Object,get()方法被重新定義為返回一個E,替換了以前的Object。
在java5.0中,當我們申明一個List或者創建一個ArrayList的實例的時候,我們需要在泛型類型的名字后面緊跟一對“<>”,尖括號中寫入我們需要的實際的類型。比如,一個保持String的List應該寫成“List<String>”。需要注意的是,這非常象給一個方法傳一個參數,區別是我們使用類型而不是值,同時使用尖括號而不是圓括號
Java.util的集合類中的元素必須是對象化的,他們不能是基本類型。泛型的引入并沒有改變這點。泛型不能使用基本類型:我們不能這樣來申明——Set<char>或者List<int>。記住,無論如何,java5.0中的自動打包和自動解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。(查看第二章以了解更多關于自動打包和自動解包的細節)。
在Java5.0中,上面的例子將被重寫為如下方式:
public static void main(String[] args) {
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won't let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
}
值得注意的是代碼量其實并沒有比原來那個沒有泛型的例子少多少。使用“(String)”這樣的類型轉換被替換成了類型參數“<String>”。 不同的是類型參數需要且僅需要聲明一次,而list能夠被使用任何多次,不需要類型轉換。在更長點的例子代碼中,這一點將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長的例子里,使用泛型依然是非常有價值的——額外的類型信息允許編譯器在您的代碼里執行更強的錯誤檢查。以前只能在運行起才能發現的錯誤現在能夠在編譯時就被發現。此外,以前為了處理類型轉換的異常,我們需要添加額外的代碼行。如果沒有泛型,那么當發生類型轉換異常的時候,一個ClassCastException異常就會被從實際代碼中拋出。
就像一個方法可以使用任意數量的參數一樣,類允許使用多個類型變量。接口Java.util.Map就是一個例子。一個Map體現了從一個key的對象到一個value的對象的映射關系。接口Map申明了一個類型變量來描述key的類型而另一個類型變量來描述value的類型。舉個例子來說,假設您希望做一個String對象到Integer對象的映射關系:
public static void main(String[] args) {
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++) map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist
// in the map
int pos = map.get("world");
}
象List<String>這個一個參數類型其本身也是也一個類型,也能夠被用于當作其他類型的一個類型變量值。您可能會看到這樣的代碼:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
在上面的代碼里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個類型為E的list元素或者一個類型為V的map元素。注意,無論如何,泛型類型能夠更精密的使用他們的變量。在本書中的參考章節查看List<E>,您將會看到它的iterator( )方法被聲明為返回一個Iterator<E>。這意味著,這個方法返回一個跟list的實際的參數類型一樣的一個參數類型的實例。為了具體的說明這點,下面的例子提供了不使用get(0)方法來獲取一個List<String>的第一個元素的方法。
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
理解泛型類型
本段將對泛型類型的使用細節做進一步的探討,以嘗試說明下列問題:
不帶類型參數的使用泛型的后果
參數化類型的體系
一個關于編譯期泛型類型的類型安全的漏洞和一個用于確保運行期類型安全的補丁
為什么參數化類型的數組不是類型安全的
未經處理的類型和不被檢查的警告
即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時候您也不被要求說明類型變量。一個不帶類型變量的泛型類型被認為是一個未經處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運行:您顯式的編寫所有類型轉換就像您已經這樣寫的一樣,您可能會被一些來自編譯器的麻煩所困擾。查看下列存儲不同類型的對象到一個未經處理的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
這段代碼在java1.4下運行得很好。如果您用java5.0來編譯它,javac編譯了,但是會打印出這樣的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint參數后重新編譯,我們會看到這些警告:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
^
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
^
編譯在add()方法的調用上給出了警告,因為它不能夠確信加入到list中的值具有正確的類型。它告訴我們說我們使用了一個未經處理的類型,它不能驗證我們的代碼是類型安全的。注意,get()方法的調用是沒有問題的,因為能夠被獲得的元素已經安全的存在于list中了。
如果您不想使用任何的java5.0的新特性,您可以簡單的通過帶-source1.4標記來編譯他們,這樣編譯器就不會再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過使用一個“@SuppressWarnings("unchecked")”注解(查看本章的4.3節)隱瞞這些警告信息或者升級您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時候不再會有警告但仍然允許您往list中放入不同的類型的對象。
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
參數化類型的體系
參數化類型有類型體系,就像一般的類型一樣。這個體系基于對象的類型,而不是變量的類型。這里有些例子您可以嘗試:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
一個List<Integer>是一個Collection<Integer>,但不是一個List<Object>。這句話不容易理解,如果您想理解為什么泛型這樣做,這段值得看一下。考察這段代碼:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can't cast a String to Integer!
這就是為什么List<Integer>不是一個List<Object>的原因,雖然List<Integer>中所有的元素事實上是一個Object的實例。如果允許轉換成List<Object>,那么轉換后,理論上非整型的對象也將被允許添加到list中。
運行時類型安全
就像我們所見到的,一個List<X>不允許被轉換為一個List<Y>,即使這個X能夠被轉換為Y。然而,一個List<X>能夠被轉換為一個List,這樣您就可以通過繼承的方法來做這樣的事情。
這種將參數化類型轉換為非參數化類型的能力對于向下兼容是必要的,但是它會在泛型所帶來的類型安全體系上鑿個漏洞:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
泛型僅提供了編譯期的類型安全。如果您使用java5.0的編譯器來編譯您的代碼并且沒有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運行期也是類型安全的。如果您獲得了警告或者使用了像未經處理的類型那樣修改您的集合的代碼,那么您需要增加一些步驟來確保運行期的類型安全。您可以通過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個wrapper集合,從而在運行時檢查確認只有正確類型的值能夠被置入集合眾。下面是一個能夠補上類型安全漏洞的一個例子:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
參數化類型的數組
在使用泛型類型的時候,數組需要特別的考慮。回憶一下,如果T是S的父類(或者接口),那么類型為S的數組S[],同時又是類型為T的數組T[]。正因為如此,每次您存放一個對象到數組中時,Java解釋器都必須進行檢查以確保您放入的對象類型與要存放的數組所允許的類型是匹對的。例如,下列代碼在運行期會檢查失敗,拋出一個ArrayStoreException異常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
雖然編譯時obj是一個Object[],但是在運行時它是一個String[],它不允許被用于存放一個Integer.
當我們使用泛型類型的時候,僅僅依靠運行時的數組存放異常檢查是不夠的,因為一個運行時進行的檢查并不能夠獲取編譯時的類型參數信息。查看下列代碼:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
如果上面的代碼被允許,那么運行時的數組存儲檢查將會成功:沒有編譯時的類型參數,代碼簡單地存儲一個ArrayList到一個ArrayList[]數組,非常正確。既然編譯器不能阻止您通過這個方法來戰勝類型安全,那么它轉而阻止您創建一個參數化類型的數組。所以上述情節永遠不會發生,編譯器在第一行就開始拒絕編譯了。
注意這并不是一個在使用數組時使用泛型的全部的約束,這僅僅是一個創建一個參數化類型數組的約束。我們將在學習如何寫泛型方法時再來討論這個話題。
類型參數通配符
假設我們需要寫一個方法來顯示一個List中的元素。[3]在以前,我們只需要象這樣寫段代碼:
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
在Java5.0中,List是一個泛型類型,如果我們試圖編譯這個方法,我們將會得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個方法:
public static void printList(PrintWriter out, List<Object> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
這段代碼能夠編譯通過同時不會有警告,但是它并不是非常地有效,因為只有那些被聲明為List<Object>的list才會被允許使用這個方法。還記得么,類似于List<String>和List<Integer>這樣的List并不能被轉型為List<Object>。事實上我們需要一個類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關心它被參數化為什么。解決辦法是使用類型參數通配符。方法可以被修改成這樣:
public static void printList(PrintWriter out, List<?> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
Object o = list.get(i);
out.print(o.toString());
}
}
這個版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。通配符“?”表示一個未知類型,類型List<?>被讀作“List of unknown”
作為一般原則,如果類型是泛型的,同時您并不知道或者并不關心值的類型,您應該使用“?”通配符來代替一個未經處理的類型。未經處理的類型被允許僅是為了向下兼容,而且應該只能夠被允許出現在老的代碼中。注意,無論如何,您不能在調用構造器時使用通配符。下面的代碼是非法的:
List<?> l = new ArrayList<?>();
創建一個不知道類型的List是毫無道理的。如果您創建了它,那么您必須知道它將保持的元素是什么類型的。您可以在隨后的方法中不關心元素類型而去遍歷這里list,但是您需要在您創建它的時候描述元素的類型。如果你確實需要一個List來保持任何類型,那么您只能這么寫:
List<Object> l = new ArrayList<Object>();
從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個未經處理的List。一個使用通配符的List<?>有兩個重要的特性。第一,考察類似于get()的方法,他們被聲明返回一個值,這個值的類型是類型參數中指定的。在這個例子中,類型是“unknown”,所以這些方法返回一個Object。既然我們期望的是調用這個object的toString()方法,程序能夠很好的滿足我們的意愿。
第二,考察List的類似add()的方法,他們被聲明為接受一個參數,這個參數被類型參數所定義。出人意料的是,當類型參數是未確定的,編譯器不允許您調用任何有不確定參數類型的方法——因為它不能確認您傳入了一個恰當的值。一個List(?)實際上是只讀的——既然編譯器不允許我們調用類似于add(),set(),addAll()這類的方法。
界定通配符
讓我們在我們原來的例子上作些小小的稍微復雜一點的改動。假設我們希望寫一個sumList()方法來計算list中Number類型的值的合計。在以前,我們使用未經處理的List,但是我們不想放棄類型安全,同時不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能調用List<Integer>、List<Double>中的方法了,而事實上我們需要調用。如果我們使用通配符,那么我們實際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調用,Number?還是Number的子類?甚至,String?這樣的一個方法也許會被寫成這樣:
public static double sumList(List<?> list) {
double total = 0.0;
for(Object o : list) {
Number n = (Number) o; // A cast is required and may fail
total += n.doubleValue();
}
return total;
}
要修改這個方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數是未知的,但又是Number或者Number的子類。下面的代碼才是我們想要的:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}
類型List<? extends Number>可以被理解為“Number未知子類的List”。理解這點非常重要,在這段文字中,Number被認為是其自身的子類。
注意,這樣的話,那些類型轉換已經不再需要了。我們并不知道list中元素的具體類型,但是我們知道他們能夠向上轉型為Number,因此我們可以把他們從list中把他們當作一個Number對象取出。使用一個for/in循環能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當您使用一個界定通配符時,類似于List中的get()方法的那些方法將返回一個類型為上界的值。因此如果我們在for/in循環中調用list.get(),我們將得到一個Number。在前一節說到使用通配符時類似于list.add()這種方法中的限制依然有效:舉個例子來說,如果編譯器允許我們調用這類方法,我們就可以將一個Integer放到一個聲明為僅保持Short值的list中去。
同樣可行的是使用下界通配符,不同的是用super替換extends。這個技巧在被調用的方法上有一點不同的作用。在實際應用中,下界通配符要比上界通配符用得少。我們將在后面的章節里討論這個問題。
腳注
[1] 在本章中,我會堅持用術語”泛型類型”來指一個聲明一個或多個類型變量的類型,用”參數化的類型”來指由實際類型參數來替換其類型變量的泛型類型。然而,在一般情況下,這種區別并不明顯,并且這些術語有時通用。
[2] 在撰寫本文時候,javac并不支持@SuppressWarnings 的注解。期望在Java 5.1中得到支持。
[3] 本節所示的3個printList()方法忽略了這樣一個事實,即java.util 中List的所有實現類都有一個可用的toString()方法。還要注意這些方法假定List實現RandomAccess并在LinkedList實例中只提供了很差的運行效率。
David Flanagan是眾多O'Reilly書籍的作者。這些書包括《Java in a Nutshell》,《Java Examples in a Nutshell》,《Java Foundation Classes in a Nutshell》,《JavaScript: The Definitive Guide》,《JavaScript Pocket Reference》。
在Java in a Nutshell, 5th Edition中查看目錄信息。
泛型是 Sun 公司發布的 JDK 5.0 中的一個重要特性,它的最大優點是提供了程序的類型安全同可以向后兼容。為了幫助讀者更好地理解和使用泛型,本文通過一些示例從基本原理,重要概念,關鍵技術,以及相似技術比較等多個角度對 Java 語言中的泛型技術進行了介紹,重點強調了泛型中的一些基本但又不是很好理解的概念。
為了避免和 C++ 中的模板混淆,本文簡要介紹了 Java 中的泛型和 C++ 中的模板的主要區別,希望這種比較能夠幫助讀者加深對泛型的理解。
引言
很多 Java 程序員都使用過集合(Collection),集合中元素的類型是多種多樣的,例如,有些集合中的元素是 Byte 類型的,而有些則可能是 String 類型的,等等。Java 語言之所以支持這么多種類的集合,是因為它允許程序員構建一個元素類型為 Object 的 Collection,所以其中的元素可以是任何類型。
當使用 Collection 時,我們經常要做的一件事情就是要進行類型轉換,當轉換成所需的類型以后,再對它們進行處理。很明顯,這種設計給編程人員帶來了極大的不便,同時也容易引入錯誤。
在很多 Java 應用中,上述情況非常普遍,為了解決這個問題,使 Java 語言變得更加安全好用,近些年的一些編譯器對 Java 語言進行了擴充,使 Java 語言支持了"泛型",特別是 Sun 公司發布的 JDK 5.0 更是將泛型作為其中一個重要的特性加以推廣。
本文首先對泛型的基本概念和特點進行簡單介紹,然后通過引入幾個實例來討論帶有泛型的類,泛型中的子類型,以及范化方法和受限類型參數等重要概念。為了幫助讀者更加深刻的理解并使用泛型,本文還介紹了泛型的轉化,即,如何將帶有泛型的 Java 程序轉化成一般的沒有泛型的 Java 程序。這樣,讀者對泛型的理解就不會僅僅局限在表面上了。考慮到多數讀者僅僅是使用泛型,因此本文并未介紹泛型在編譯器中的具體實現。Java 中的泛型和 C++ 中的模板表面上非常相似,但實際上二者還是有很大區別的,本文最后簡單介紹了 Java 中的泛型與 C++ 模板的主要區別。
泛型概覽
泛型本質上是提供類型的"類型參數",它們也被稱為參數化類型(parameterized type)或參量多態(parametric polymorphism)。其實泛型思想并不是 Java 最先引入的,C++ 中的模板就是一個運用泛型的例子。
GJ(Generic Java)是對 Java 語言的一種擴展,是一種帶有參數化類型的 Java 語言。用 GJ 編寫的程序看起來和普通的 Java 程序基本相同,只不過多了一些參數化的類型同時少了一些類型轉換。實際上,這些 GJ 程序也是首先被轉化成一般的不帶泛型的 Java 程序后再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯。具體的轉化過程大致分為以下幾個部分:
- 將參數化類型中的類型參數"擦除"(erasure)掉;
- 將類型變量用"上限(upper bound)"取代,通常情況下這些上限是 Object。這里的類型變量是指實例域,本地方法域,方法參數以及方法返回值中用來標記類型信息的"變量",例如:實例域中的變量聲明
A elem;
,方法聲明 Node (A elem){};
,其中,A 用來標記 elem 的類型,它就是類型變量。
- 添加類型轉換并插入"橋方法"(bridge method),以便覆蓋(overridden)可以正常的工作。
轉化后的程序和沒有引入泛型時程序員不得不手工完成轉換的程序是非常一致的,具體的轉化過程會在后面介紹。GJ 保持了和 Java 語言以及 Java 虛擬機很好的兼容性,下面對 GJ 的特點做一個簡要的總結。
- 類型安全。 泛型的一個主要目標就是提高 Java 程序的類型安全。使用泛型可以使編譯器知道變量的類型限制,進而可以在更高程度上驗證類型假設。如果沒有泛型,那么類型的安全性主要由程序員來把握,這顯然不如帶有泛型的程序安全性高。
- 消除強制類型轉換。泛型可以消除源代碼中的許多強制類型轉換,這樣可以使代碼更加可讀,并減少出錯的機會。
- 向后兼容。支持泛型的 Java 編譯器(例如 JDK5.0 中的 Javac)可以用來編譯經過泛型擴充的 Java 程序(GJ 程序),但是現有的沒有使用泛型擴充的 Java 程序仍然可以用這些編譯器來編譯。
- 層次清晰,恪守規范。無論被編譯的源程序是否使用泛型擴充,編譯生成的字節碼均可被虛擬機接受并執行。也就是說不管編譯器的輸入是 GJ 程序,還是一般的 Java 程序,經過編譯后的字節碼都嚴格遵循《Java 虛擬機規范》中對字節碼的要求。可見,泛型主要是在編譯器層面實現的,它對于 Java 虛擬機是透明的。
- 性能收益。目前來講,用 GJ 編寫的代碼和一般的 Java 代碼在效率上是非常接近的。 但是由于泛型會給 Java 編譯器和虛擬機帶來更多的類型信息,因此利用這些信息對 Java 程序做進一步優化將成為可能。
以上是泛型的一些主要特點,下面通過幾個相關的例子來對 Java 語言中的泛型進行說明。
帶有泛型的類
為了幫助大家更好地理解 Java 語言中的泛型,我們在這里先來對比兩段實現相同功能的 GJ 代碼和 Java 代碼。通過觀察它們的不同點來對 Java 中的泛型有個總體的把握,首先來分析一下不帶泛型的 Java 代碼,程序如下:
1 interface Collection {
2 public void add (Object x);
3 public Iterator iterator ();
4 }
5
6 interface Iterator {
7 public Object next ();
8 public boolean hasNext ();
9 }
10
11 class NoSuchElementException extends RuntimeException {}
12
13 class LinkedList implements Collection {
14
15 protected class Node {
16 Object elt;
17 Node next = null;
18 Node (Object elt) { this.elt = elt; }
19 }
20
21 protected Node head = null, tail = null;
22
23 public LinkedList () {}
24
25 public void add (Object elt) {
26 if (head == null) { head = new Node(elt); tail = head; }
27 else { tail.next = new Node(elt); tail = tail.next; }
28 }
29
30 public Iterator iterator () {
31
32 return new Iterator () {
33 protected Node ptr = head;
34 public boolean hasNext () { return ptr != null; }
35 public Object next () {
36 if (ptr != null) {
37 Object elt = ptr.elt; ptr = ptr.next; return elt;
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
38 } else throw new NoSuchElementException ();
39 }
40 };
41 }
42 }
|
接口 Collection
提供了兩個方法,即添加元素的方法 add(Object x)
,見第 2 行,以及返回該 Collection
的 Iterator
實例的方法 iterator()
,見第 3 行。Iterator
接口也提供了兩個方法,其一就是判斷是否有下一個元素的方法 hasNext()
,見第 8 行,另外就是返回下一個元素的方法 next()
,見第 7 行。LinkedList
類是對接口 Collection
的實現,它是一個含有一系列節點的鏈表,節點中的數據類型是 Object,這樣就可以創建任意類型的節點了,比如 Byte, String 等等。上面這段程序就是用沒有泛型的傳統的 Java 語言編寫的代碼。接下來我們分析一下傳統的 Java 語言是如何使用這個類的。
代碼如下:
1 class Test {
2 public static void main (String[] args) {
3 // byte list
4 LinkedList xs = new LinkedList();
5 xs.add(new Byte(0)); xs.add(new Byte(1));
6 Byte x = (Byte)xs.iterator().next();
7 // string list
8 LinkedList ys = new LinkedList();
9 ys.add("zero"); ys.add("one");
10 String y = (String)ys.iterator().next();
11 // string list list
12 LinkedList zss = new LinkedList();
13 zss.add(ys);
14 String z = (String)((LinkedList)zss.iterator().next()).iterator().next();
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
15 // string list treated as byte list
16 Byte w = (Byte)ys.iterator().next(); // run-time exception
17 }
18 }
|
從上面的程序我們可以看出,當從一個鏈表中提取元素時需要進行類型轉換,這些都要由程序員顯式地完成。如果我們不小心從 String 類型的鏈表中試圖提取一個 Byte 型的元素,見第 15 到第 16 行的代碼,那么這將會拋出一個運行時的異常。請注意,上面這段程序可以順利地經過編譯,不會產生任何編譯時的錯誤,因為編譯器并不做類型檢查,這種檢查是在運行時進行的。不難發現,傳統 Java 語言的這一缺陷推遲了發現程序中錯誤的時間,從軟件工程的角度來看,這對軟件的開發是非常不利的。接下來,我們討論一下如何用 GJ 來實現同樣功能的程序。源程序如下:
1 interface Collection<A> {
2 public void add(A x);
3 public Iterator<A> iterator();
4 }
5
6 interface Iterator<A> {
7 public A next();
8 public boolean hasNext();
9 }
10
11 class NoSuchElementException extends RuntimeException {}
12
13 class LinkedList<A> implements Collection<A> {
14 protected class Node {
15 A elt;
16 Node next = null;
17 Node (A elt) { this.elt = elt; }
18 }
19
20 protected Node head = null, tail = null;
21
22 public LinkedList () {}
23
24 public void add (A elt) {
25 if (head == null) { head = new Node(elt); tail = head; }
26 else { tail.next = new Node(elt); tail = tail.next; }
27 }
28
29 public Iterator<A> iterator () {
30 return new Iterator<A> () {
31 protected Node ptr = head;
32 public boolean hasNext () { return ptr != null; }
33 public A next () {
34 if (ptr != null) {
35 A elt = ptr.elt; ptr = ptr.next; return elt;
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
36 } else throw new NoSuchElementException ();
37 }
38 };
39 }
40 }
|
程序的功能并沒有任何改變,只是在實現方式上使用了泛型技術。我們注意到上面程序的接口和類均帶有一個類型參數 A,它被包含在一對尖括號(< >)中,見第 1,6 和 13 行,這種表示法遵循了 C++ 中模板的表示習慣。這部分程序和上面程序的主要區別就是在 Collection
, Iterator
, 或 LinkedList
出現的地方均用 Collection<A>
, Iterator<A>
, 或 LinkedList<A>
來代替,當然,第 22 行對構造函數的聲明除外。
下面再來分析一下在 GJ 中是如何對這個類進行操作的,程序如下:
1 class Test {
2 public static void main (String [] args) {
3 // byte list
4 LinkedList<Byte> xs = new LinkedList<Byte>();
5 xs.add(new Byte(0)); xs.add(new Byte(1));
6 Byte x = xs.iterator().next();
7 // string list
8 LinkedList<String> ys = new LinkedList<String>();
9 ys.add("zero"); ys.add("one");
10 String y = ys.iterator().next();
11 // string list list
12 LinkedList<LinkedList<String>>zss=
newLinkedList<LinkedList<String>>();
13 zss.add(ys);
14 String z = zss.iterator().next().iterator().next();
15 // string list treated as byte list
16 Byte w = ys.iterator().next(); // compile-time error
17 }
18 }
|
在這里我們可以看到,有了泛型以后,程序員并不需要進行顯式的類型轉換,只要賦予一個參數化的類型即可,見第 4,8 和 12 行,這是非常方便的,同時也不會因為忘記進行類型轉換而產生錯誤。另外需要注意的就是當試圖從一個字符串類型的鏈表里提取出一個元素,然后將它賦值給一個 Byte 型的變量時,見第 16 行,編譯器將會在編譯時報出錯誤,而不是由虛擬機在運行時報錯,這是因為編譯器會在編譯時刻對 GJ 代碼進行類型檢查,此種機制有利于盡早地發現并改正錯誤。
類型參數的作用域是定義這個類型參數的整個類,但是不包括靜態成員函數。這是因為當訪問同一個靜態成員函數時,同一個類的不同實例可能有不同的類型參數,所以上述提到的那個作用域不應該包括這些靜態函數,否則就會引起混亂。
泛型中的子類型
在 Java 語言中,我們可以將某種類型的變量賦值給其父類型所對應的變量,例如,String 是 Object 的子類型,因此,我們可以將 String 類型的變量賦值給 Object 類型的變量,甚至可以將 String [ ] 類型的變量(數組)賦值給 Object [ ] 類型的變量,即 String [ ] 是 Object [ ] 的子類型。
上述情形恐怕已經深深地印在了廣大讀者的腦中,對于泛型來講,上述情形有所變化,因此請廣大讀者務必引起注意。為了說明這種不同,我們還是先來分析一個小例子,代碼如下所示:
1 List<String> ls = new ArrayList<String>();
2 List<Object> lo = ls;
3 lo.add(new Integer());
4 String s = ls.get(0);
|
上述代碼的第二行將 List<String>
賦值給了 List<Object>
,按照以往的經驗,這種賦值好像是正確的,因為 List<String>
應該是 List<Object>
的子類型。這里需要特別注意的是,這種賦值在泛型當中是不允許的!List<String>
也不是 List<Object>
的子類型。
如果上述賦值是合理的,那么上面代碼的第三行的操作將是可行的,因為 lo
是 List<Object>
,所以向其添加 Integer 類型的元素應該是完全合法的。讀到此處,我們已經看到了第二行的這種賦值所潛在的危險,它破壞了泛型所帶來的類型安全性。
一般情況下,如果 A 是 B 的子類型,C 是某個泛型的聲明,那么 C<A>
并不是 C<B>
的子類型,我們也不能將 C<A>
類型的變量賦值給 C<B>
類型的變量。這一點和我們以前接觸的父子類型關系有很大的出入,因此請讀者務必引起注意。
泛化方法和受限類型參數
在這一部分我們將討論有關泛化方法(generic method )和受限類型參數(bounded type parameter)的內容,這是泛型中的兩個重要概念,還是先來分析一下與此相關的代碼。
1 interface Comparable<A> {
2 public int compareTo(A that);
3 }
4
5 class Byte implements Comparable<Byte> {
6 private byte value;
7 public Byte(byte value) {this.value = value;}
8 public byte byteValue() {return value;}
9 public int compareTo(Byte that) {
10 return this.value - that.value;
11 }
12 }
13
14 class Collections {
15 public static <A implements Comparable<A>>
16 A max (Collection<A> xs) {
17 Iterator<A> xi = xs.iterator();
18 A w = xi.next();
19 while (xi.hasNext()) {
20 A x = xi.next();
21 if (w.compareTo(x) < 0) w = x;
22 }
23 return w;
24 }
25 }
|
這里定義了一個接口 Comparable<A>
,用來和 A 類型的對象進行比較。類 Byte 實現了這個接口,并以它自己作為類型參數,因此,它們自己就可以和自己進行比較了。
第 14 行到第 25 行的代碼定義了一個類 Collections
,這個類包含一個靜態方法 max(Collection<A> xs)
,它用來在一個非空的 Collection
中尋找最大的元素并返回這個元素。這個方法的兩個特點就是它是一個泛化方法并且有一個受限類型參數。
之所以說它是泛化了的方法,是因為這個方法可以應用到很多種類型上。當要將一個方法聲明為泛化方法時,我們只需要在這個方法的返回類型(A)之前加上一個類型參數(A),并用尖括號(< >)將它括起來。這里的類型參數(A)是在方法被調用時自動實例化的。例如,假設對象 m 的類型是 Collection<Byte>
,那么當使用下面的語句:
Byte x = Collections.max(m);
|
調用方法 max 時,該方法的參數 A 將被推測為 Byte。
根據上面討論的內容,泛化方法 max 的完整聲明應該是下面的形式:
< A > A max (Collection<A> xs) {
max 的方法體
}
|
但是,我們見到的 max 在 < A > 中還多了 "implements Comparable<A>" 一項,這是什么呢?這就是我們下面將要談到的"受限的類型參數"。在上面的例子中,類型參數 A 就是一個受限的的類型參數,因為它不是泛指任何類型,而是指那些自己和自己作比較的類型。例如參數可以被實例化為 Byte,因為程序中有 Byte implements Comparable<Byte>
的語句,參見第 5 行。這種限制(或者說是范圍)通過如下的方式表示,"類型參數 implements 接口",或是 "類型參數 extend 類",上面程序中的"Byte implements Comparable<Byte>"就是一例。
泛型的轉化
在前面的幾部分內容當中,我們介紹了有關泛型的基礎知識,到此讀者對 Java 中的泛型技術應該有了一定的了解,接下來的這部分內容將討論有關泛型的轉化,即如何將帶有泛型的 Java 代碼轉化成一般的沒有泛型 Java 代碼。其實在前面的部分里,我們或多或少地也提到了一些相關的內容,下面再來詳細地介紹一下。
首先需要明確的一點是上面所講的這種轉化過程是由編譯器(例如:Javac)完成的,虛擬機并不負責完成這一任務。當編譯器對帶有泛型的 Java 代碼進行編譯時,它會去執行類型檢查和類型推斷,然后生成普通的不帶泛型的字節碼,這種字節碼可以被一般的Java虛擬機接收并執行,這種技術被稱為擦除(erasure)。
可見,編譯器可以在對源程序(帶有泛型的 Java 代碼)進行編譯時使用泛型類型信息保證類型安全,對大量如果沒有泛型就不會去驗證的類型安全約束進行驗證,同時在生成的字節碼當中,將這些類型信息清除掉。
對于不同的情況,擦除技術所執行的"擦除"動作是不同的,主要分為以下幾種情況:
- 對于參數化類型,需要刪除其中的類型參數,例如,
LinkedList<A>
將被"擦除"為 LinkedList;
- 對于非參數化類型,不作擦除,或者說用它自己來擦除自己,例如 String 將被"擦除"為 String;
- 對于類型變量(有關類型變量的說明請參考"泛型概覽"相關內容),要用它們的上限來對它們進行替換。多數情況下這些上限是 Object,但是也有例外,后面的部分將會對此進行介紹。
除此之外,還需要注意的一點是,在某些情況下,擦除技術需要引入類型轉換(cast),這些情況主要包括:
情況 1. 方法的返回類型是類型參數;
情況 2. 在訪問數據域時,域的類型是一個類型參數。
例如在本文"帶有泛型的類"一小節的最后,我們給出了一段測試程序,一個 Test 類。這個類包含以下幾行代碼:
8 LinkedList<String> ys = new LinkedList<String>();
9 ys.add("zero"); ys.add("one");
10 String y = ys.iterator().next();
這部分代碼轉換后就變成了如下的代碼:
8 LinkedList ys = new LinkedList();
9 ys.add("zero"); ys.add("one");
10 String y = (String)ys.iterator().next();
|
第 10 行的代碼進行了類型轉換,這是因為在調用 next()
方法時,編譯器發現該方法的返回值類型是類型參數 A(請參見對方法 next()
的定義),因此根據上面提到的情況 1,需要進行類型轉換。
上面介紹了泛型轉化中的擦除技術,接下來,我們討論一下泛型轉化中的另外一個重要問題--橋方法(bridge method)。
Java 是一種面向對象的語言,因此覆蓋(overridden)是其中的一項重要技術。覆蓋能夠正常"工作"的前提是方法名和方法的參數類型及個數完全匹配(參數的順序也應一致),為了滿足這項要求,編譯器在泛型轉化中引入了橋方法(bridge method)。接下來,我們通過一個例子來分析一下橋方法在泛型轉化中所起的作用。在本文"泛化方法和受限類型參數"一小節所給出的代碼中,第 9 行到第 11 行的程序如下所示:
9 public int compareTo(Byte that) {
10 return this.value - that.value;
11 }
這部分代碼經過轉化,就變成了下面的樣子:
9 public int compareTo(Byte that) {
10 return this.value - that.value;
11 }
12 public int compareTo(Object that){
13 return this.compareTo((Byte)that);
14 }
|
第 12 行的方法 compareTo(Object that)
就是一個橋方法,在這里引入這個方法是為了保證覆蓋能夠正常的發生。我們在前面提到過,覆蓋必須保證方法名和參數的類型及數目完全匹配,在這里通過引入這個"橋"即可達到這一目的,由這個"橋"進行類型轉換,并調用第 9 行參數類型為 Byte 的方法 compareTo(Byte that),需要注意的一點是這里的 "Object" 也并不一定是完全匹配的類型,但由于它是 Java 語言中類層次結構的根,所以這里用 "Object" 可以接受其他任何類型的參數。
根據面向對象的基本概念,我們知道,重載(overloading)允許橋方法和原來的方法共享同一個方法名,正如上面例子所顯示的那樣,因此橋方法的引入是完全合法的。一般情況下,當一個類實現了一個參數化的接口或是繼承了一個參數化的類時,需要引入橋方法。
到此,我們對泛型中的子類型,帶有泛型的類,泛化方法,受限類型參數以及泛型的轉化進行了簡要的介紹,下面部分將結合這些技術對前面提到的例子進行一下總結,以便能夠幫助讀者更深刻更全面地理解泛型。
首先來分析一下本文提到的那個 Collection
的例子。這里先是定義了兩個接口 Collection
和 Iterator
,然后又定義了一個對接口 Collection
的一個實現 LinkedList
。根據上面所介紹的對泛型的轉化過程,這段代碼轉化后的 Java 程序為:
1 interface Collection {
2 public void add (Object x);
3 public Iterator iterator ();
4 }
5
6 interface Iterator {
7 public Object next ();
8 public boolean hasNext ();
9 }
10
11 class NoSuchElementException extends RuntimeException {}
12
13 class LinkedList implements Collection {
14
15 protected class Node {
16 Object elt;
17 Node next = null;
18 Node (Object elt) { this.elt = elt; }
19 }
20
21 protected Node head = null, tail = null;
22
23 public LinkedList () {}
24
25 public void add (Object elt) {
26 if (head == null) {
27 head = new Node(elt); tail = head;
28 } else {
29 tail.next = new Node(elt); tail = tail.next;
30 }
31 }
32
33 public Iterator iterator () {
34 return new Iterator () {
35 protected Node ptr = head;
36 public boolean hasNext () { return ptr != null; }
37 public Object next () {
38 if (ptr != null) {
39 Object elt = ptr.elt; ptr = ptr.next; return elt;
40 } else {
41 throw new NoSuchElementException ();
42 }
43 }
44 };
45 }
46 }
|
通過分析上述代碼,我們不難發現,所有參數化類型 Collection, Iterator 和 LinkedList 中的類型參數 "A" 全都被擦除了。另外,剩下的類型變量 "A" 都用其上限進行了替換,這里的上限是 Object,見黑體字標出的部分,這是轉化的關鍵部分。
下面我們分析一下在介紹有關泛化方法(generic method)和受限類型參數(bounded type parameter)時舉的那個例子,該段 GJ 代碼經過轉換后的等價 Java 程序如下所示:
1 interface Comparable {
2 public int compareTo(Object that);
3 }
4
5 class Byte implements Comparable {
6 private byte value;
7 public Byte(byte value) {this.value = value;}
8 public byte byteValue(){return value;}
9 public int compareTo(Byte that) {
10 return this.value - that.value;
11 }
12 public int compareTo(Object that){
13 return this.compareTo((Byte)that);
14 }
15 }
16
17 class Collections {
18 public static Comparable max(Collection xs){
19 Iterator xi = xs.iterator();
20 Comparable w = (Comparable)xi.next();
21 while (xi.hasNext()) {
22 Comparable x = (Comparable)xi.next();
23 if (w.compareTo(x) < 0) w = x;
23 }
24 return w;
25 }
26 }
|
同樣請讀者注意黑體字標出的部分,這些關鍵點我們在前面已經介紹過了,故不贅述。唯一需要注意的一點就是第 18,20,22 行出現的Comparable。在泛型轉化中,類型變量應該用其上限來替換,一般情況下這些上限是 "Object",但是當遇到受限的類型參數時,這個上限就不再是 "Object" 了,編譯器會用限制這些類型參數的類型來替換它,上述代碼就用了對 A 進行限制的類型 "Comparable" 來替換 A。
橋方法的引入,為解決覆蓋問題帶來了方便,但是這種方法還存在一些問題,例如下面這段代碼:
1 interface Iterator<A> {
2 public boolean hasNext ();
3 public A next ();
4 }
5 class Interval implements Iterator<Integer> {
6 private int i;
7 private int n;
8 public Interval (int l, int u) { i = l; n = u; }
9 public boolean hasNext () { return (i <= n); }
10 public Integer next () { return new Integer(i++); }
11 }
|
根據以上所講的內容,這部分代碼轉換后的 Java 程序應該是如下這個樣子:
1 interface Iterator {
2
3 public boolean hasNext ();
4 public Object next ();
5
6 }
7
8 class Interval implements Iterator {
9
10 private int i;
11 private int n;
12 public Interval (int l, int u) { i = l; n = u; }
13 public boolean hasNext () { return (i <= n); }
14 public Integer next%1% () { return new Integer(i++); }
15 // bridge
16 public Object next%2%() { return next%1%(); }
17
18 }
|
相信有些讀者已經發現了這里的問題,這不是一段合法的 Java 源程序,因為第 14 行和第 16 行的兩個 next() 有相同的參數,無法加以區分。代碼中的 %1% 和 %2% 是為了區分而人為加入的,并非 GJ 轉化的結果。
不過,這并不是什么太大的問題,因為 Java 虛擬機可以區分這兩個 next() 方法,也就是說,從 Java 源程序的角度來看,上述程序是不正確的,但是當編譯成字節碼時,JVM 可以對兩個 next() 方法進行識別。這是因為,在 JVM 中,方法定義時所使用的方法簽名包括方法的返回類型,這樣一來,只要 GJ 編譯出的字節碼符合Java字節碼的規范即可,這也正好說明了 GJ 和 JVM 中字節碼規范要求的一致性!
最后,值得一提的是,JDK 5.0 除了在編譯器層面對 Java 中的泛型進行了支持,Java 的類庫為支持泛型也做了相應地調整,例如,集合框架中所有的標準集合接口都進行了泛型化,同時,集合接口的實現也都進行了相應地泛型化。
Java 中的泛型與 C++ 模板的比較
GJ 程序的語法在表面上與 C++ 中的模板非常類似,但是二者之間有著本質的區別。
首先,Java 語言中的泛型不能接受基本類型作為類型參數――它只能接受引用類型。這意味著可以定義 List<Integer>,但是不可以定義 List<int>。
其次,在 C++ 模板中,編譯器使用提供的類型參數來擴充模板,因此,為 List<A> 生成的 C++ 代碼不同于為 List<B> 生成的代碼,List<A> 和 List<B> 實際上是兩個不同的類。而 Java 中的泛型則以不同的方式實現,編譯器僅僅對這些類型參數進行擦除和替換。類型 ArrayList<Integer> 和 ArrayList<String> 的對象共享相同的類,并且只存在一個 ArrayList 類。
總結
本文通過一些示例從基本原理,重要概念,關鍵技術,以及相似技術比較等多個角度對 Java 語言中的泛型技術進行了介紹,希望這種介紹方法能夠幫助讀者更好地理解和使用泛型。本文主要針對廣大的 Java 語言使用者,在介紹了泛型的基本概念后,重點介紹了比較底層的泛型轉化技術,旨在幫助讀者更加深刻地掌握泛型,筆者相信這部分內容可以使讀者避免對泛型理解的表面化,也所謂知其然更知其所以然。
參考資料
關于作者