泛型類型,第一部分
作者: David Flanagan
翻譯:cat
版權(quán)聲明:可以任意轉(zhuǎn)載,轉(zhuǎn)載時(shí)請(qǐng)務(wù)必以超鏈接形式標(biāo)明文章原始出處和作者信息及本聲明
作者:
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
關(guān)鍵詞: Generic Types
編輯按:《Java in a Nutshell, 5th Edition》覆蓋了jdk5.0中很多變化和新特征,其中最重要的就是泛型。在本文的第一部分,作者David Flanagan介紹了如何使用泛型;而在第二部分,作者描述了如何寫你自己的泛型和泛型方法。
Java5.0的新特性之一是引入了泛型類型和泛型方法。一個(gè)泛型類型通過使用一個(gè)或多個(gè)類型變量來定義,并擁有一個(gè)或多個(gè)使用一個(gè)類型變量作為一個(gè)參數(shù)或者返回值的占位符。例如,類型java.util.List<E>是一個(gè)泛型類型:一個(gè)list,其元素的類型被占位符E描述。這個(gè)類型有一個(gè)名為add()的方法,被聲明為有一個(gè)類型為E的參數(shù),同時(shí),有一個(gè)get()方法,返回值被聲明為E類型。
為了使用泛型類型,你應(yīng)該為類型變量詳細(xì)指明實(shí)際的類型,形成一個(gè)就像List<String>類似的參數(shù)化類型。[1]指明這些額外的類型信息的原因是編譯器據(jù)此能夠在編譯期為您提供很強(qiáng)的類型檢查,增強(qiáng)您的程序的類型安全性。舉個(gè)例子來說,您有一個(gè)只能保持String對(duì)象的List,那么這種類型檢查就能夠阻止您往里面加入String[]對(duì)象。同樣的,增加的類型信息使編譯器能夠?yàn)槟鲆恍╊愋娃D(zhuǎn)換的事情。比如,編譯器知道了一個(gè)List<String>有個(gè)get()方法,其返回值是一個(gè)String對(duì)象,因此您不再需要去將返回值由一個(gè)Object強(qiáng)制轉(zhuǎn)換為String。
Java.util包中的集合類在java5.0中已經(jīng)被做成了泛型,也許您將會(huì)在您的程序中頻繁的使用到他們。類型安全的集合類就是一個(gè)泛型類型的典型案例。即便您從沒有定義過您自己的泛型類型甚至從未用過除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個(gè)標(biāo)志——他們證明了這個(gè)主要的新語言特性的復(fù)雜性。
我們從探索類型安全的集合類中的基本的泛型用法開始,進(jìn)而研究更多使用泛型類型的復(fù)雜細(xì)節(jié)。然后我們討論類型參數(shù)通配符和有界通配符。描繪了如何使用泛型以后,我們闡明如何編寫自己的泛型類型和泛型方法。我們對(duì)于泛型的討論將結(jié)束于一趟對(duì)于JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對(duì)泛型如何工作這個(gè)問題有個(gè)深入的理解。
類型安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對(duì)象的set、對(duì)象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數(shù)來界定集合中的對(duì)象的類型。這個(gè)討論并不適合java1.4或更早期版本。如果沒有泛型,對(duì)于集合類的使用需要程序員記住每個(gè)集合中元素的類型。當(dāng)您在java1.4種創(chuàng)建了一個(gè)集合,您知道您放入到集合中的對(duì)象的類型,但是編譯器不知道。您必須小心地往其中加入一個(gè)合適類型的元素,當(dāng)需要從集合中獲取元素時(shí),您必須顯式的寫強(qiáng)制類型轉(zhuǎn)換以將他們從Object轉(zhuǎn)換為他們真是的類型。考察下邊的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或是其他集合類已經(jīng)使用泛型重寫過了。就像前面提到的, List被重新定義為一個(gè)list,它中間的元素類型被一個(gè)類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個(gè)類型為E的參數(shù),用于替換以前的Object,get()方法被重新定義為返回一個(gè)E,替換了以前的Object。
在java5.0中,當(dāng)我們申明一個(gè)List或者創(chuàng)建一個(gè)ArrayList的實(shí)例的時(shí)候,我們需要在泛型類型的名字后面緊跟一對(duì)“<>”,尖括號(hào)中寫入我們需要的實(shí)際的類型。比如,一個(gè)保持String的List應(yīng)該寫成“List<String>”。需要注意的是,這非常象給一個(gè)方法傳一個(gè)參數(shù),區(qū)別是我們使用類型而不是值,同時(shí)使用尖括號(hào)而不是圓括號(hào)
Java.util的集合類中的元素必須是對(duì)象化的,他們不能是基本類型。泛型的引入并沒有改變這點(diǎn)。泛型不能使用基本類型:我們不能這樣來申明——Set<char>或者List<int>。記住,無論如何,java5.0中的自動(dòng)打包和自動(dòng)解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。(查看第二章以了解更多關(guān)于自動(dòng)打包和自動(dòng)解包的細(xì)節(jié))。
在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);
}
值得注意的是代碼量其實(shí)并沒有比原來那個(gè)沒有泛型的例子少多少。使用“(String)”這樣的類型轉(zhuǎn)換被替換成了類型參數(shù)“<String>”。 不同的是類型參數(shù)需要且僅需要聲明一次,而list能夠被使用任何多次,不需要類型轉(zhuǎn)換。在更長(zhǎng)點(diǎn)的例子代碼中,這一點(diǎn)將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長(zhǎng)的例子里,使用泛型依然是非常有價(jià)值的——額外的類型信息允許編譯器在您的代碼里執(zhí)行更強(qiáng)的錯(cuò)誤檢查。以前只能在運(yùn)行起才能發(fā)現(xiàn)的錯(cuò)誤現(xiàn)在能夠在編譯時(shí)就被發(fā)現(xiàn)。此外,以前為了處理類型轉(zhuǎn)換的異常,我們需要添加額外的代碼行。如果沒有泛型,那么當(dāng)發(fā)生類型轉(zhuǎn)換異常的時(shí)候,一個(gè)ClassCastException異常就會(huì)被從實(shí)際代碼中拋出。
就像一個(gè)方法可以使用任意數(shù)量的參數(shù)一樣,類允許使用多個(gè)類型變量。接口Java.util.Map就是一個(gè)例子。一個(gè)Map體現(xiàn)了從一個(gè)key的對(duì)象到一個(gè)value的對(duì)象的映射關(guān)系。接口Map申明了一個(gè)類型變量來描述key的類型而另一個(gè)類型變量來描述value的類型。舉個(gè)例子來說,假設(shè)您希望做一個(gè)String對(duì)象到Integer對(duì)象的映射關(guān)系:
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>這個(gè)一個(gè)參數(shù)類型其本身也是也一個(gè)類型,也能夠被用于當(dāng)作其他類型的一個(gè)類型變量值。您可能會(huì)看到這樣的代碼:
// 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()方法返回一個(gè)類型為E的list元素或者一個(gè)類型為V的map元素。注意,無論如何,泛型類型能夠更精密的使用他們的變量。在本書中的參考章節(jié)查看List<E>,您將會(huì)看到它的iterator( )方法被聲明為返回一個(gè)Iterator<E>。這意味著,這個(gè)方法返回一個(gè)跟list的實(shí)際的參數(shù)類型一樣的一個(gè)參數(shù)類型的實(shí)例。為了具體的說明這點(diǎn),下面的例子提供了不使用get(0)方法來獲取一個(gè)List<String>的第一個(gè)元素的方法。
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
理解泛型類型
本段將對(duì)泛型類型的使用細(xì)節(jié)做進(jìn)一步的探討,以嘗試說明下列問題:
不帶類型參數(shù)的使用泛型的后果
參數(shù)化類型的體系
一個(gè)關(guān)于編譯期泛型類型的類型安全的漏洞和一個(gè)用于確保運(yùn)行期類型安全的補(bǔ)丁
為什么參數(shù)化類型的數(shù)組不是類型安全的
未經(jīng)處理的類型和不被檢查的警告
即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時(shí)候您也不被要求說明類型變量。一個(gè)不帶類型變量的泛型類型被認(rèn)為是一個(gè)未經(jīng)處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運(yùn)行:您顯式的編寫所有類型轉(zhuǎn)換就像您已經(jīng)這樣寫的一樣,您可能會(huì)被一些來自編譯器的麻煩所困擾。查看下列存儲(chǔ)不同類型的對(duì)象到一個(gè)未經(jīng)處理的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
這段代碼在java1.4下運(yùn)行得很好。如果您用java5.0來編譯它,javac編譯了,但是會(huì)打印出這樣的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint參數(shù)后重新編譯,我們會(huì)看到這些警告:
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()方法的調(diào)用上給出了警告,因?yàn)樗荒軌虼_信加入到list中的值具有正確的類型。它告訴我們說我們使用了一個(gè)未經(jīng)處理的類型,它不能驗(yàn)證我們的代碼是類型安全的。注意,get()方法的調(diào)用是沒有問題的,因?yàn)槟軌虮猾@得的元素已經(jīng)安全的存在于list中了。
如果您不想使用任何的java5.0的新特性,您可以簡(jiǎn)單的通過帶-source1.4標(biāo)記來編譯他們,這樣編譯器就不會(huì)再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過使用一個(gè)“@SuppressWarnings("unchecked")”注解(查看本章的4.3節(jié))隱瞞這些警告信息或者升級(jí)您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時(shí)候不再會(huì)有警告但仍然允許您往list中放入不同的類型的對(duì)象。
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
參數(shù)化類型的體系
參數(shù)化類型有類型體系,就像一般的類型一樣。這個(gè)體系基于對(duì)象的類型,而不是變量的類型。這里有些例子您可以嘗試:
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
一個(gè)List<Integer>是一個(gè)Collection<Integer>,但不是一個(gè)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>不是一個(gè)List<Object>的原因,雖然List<Integer>中所有的元素事實(shí)上是一個(gè)Object的實(shí)例。如果允許轉(zhuǎn)換成List<Object>,那么轉(zhuǎn)換后,理論上非整型的對(duì)象也將被允許添加到list中。
運(yùn)行時(shí)類型安全
就像我們所見到的,一個(gè)List<X>不允許被轉(zhuǎn)換為一個(gè)List<Y>,即使這個(gè)X能夠被轉(zhuǎn)換為Y。然而,一個(gè)List<X>能夠被轉(zhuǎn)換為一個(gè)List,這樣您就可以通過繼承的方法來做這樣的事情。
這種將參數(shù)化類型轉(zhuǎn)換為非參數(shù)化類型的能力對(duì)于向下兼容是必要的,但是它會(huì)在泛型所帶來的類型安全體系上鑿個(gè)漏洞:
// 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的編譯器來編譯您的代碼并且沒有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運(yùn)行期也是類型安全的。如果您獲得了警告或者使用了像未經(jīng)處理的類型那樣修改您的集合的代碼,那么您需要增加一些步驟來確保運(yùn)行期的類型安全。您可以通過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個(gè)wrapper集合,從而在運(yùn)行時(shí)檢查確認(rèn)只有正確類型的值能夠被置入集合眾。下面是一個(gè)能夠補(bǔ)上類型安全漏洞的一個(gè)例子:
// 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");
參數(shù)化類型的數(shù)組
在使用泛型類型的時(shí)候,數(shù)組需要特別的考慮。回憶一下,如果T是S的父類(或者接口),那么類型為S的數(shù)組S[],同時(shí)又是類型為T的數(shù)組T[]。正因?yàn)槿绱耍看文娣乓粋€(gè)對(duì)象到數(shù)組中時(shí),Java解釋器都必須進(jìn)行檢查以確保您放入的對(duì)象類型與要存放的數(shù)組所允許的類型是匹對(duì)的。例如,下列代碼在運(yùn)行期會(huì)檢查失敗,拋出一個(gè)ArrayStoreException異常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
雖然編譯時(shí)obj是一個(gè)Object[],但是在運(yùn)行時(shí)它是一個(gè)String[],它不允許被用于存放一個(gè)Integer.
當(dāng)我們使用泛型類型的時(shí)候,僅僅依靠運(yùn)行時(shí)的數(shù)組存放異常檢查是不夠的,因?yàn)橐粋€(gè)運(yùn)行時(shí)進(jìn)行的檢查并不能夠獲取編譯時(shí)的類型參數(shù)信息。查看下列代碼:
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!
如果上面的代碼被允許,那么運(yùn)行時(shí)的數(shù)組存儲(chǔ)檢查將會(huì)成功:沒有編譯時(shí)的類型參數(shù),代碼簡(jiǎn)單地存儲(chǔ)一個(gè)ArrayList到一個(gè)ArrayList[]數(shù)組,非常正確。既然編譯器不能阻止您通過這個(gè)方法來戰(zhàn)勝類型安全,那么它轉(zhuǎn)而阻止您創(chuàng)建一個(gè)參數(shù)化類型的數(shù)組。所以上述情節(jié)永遠(yuǎn)不會(huì)發(fā)生,編譯器在第一行就開始拒絕編譯了。
注意這并不是一個(gè)在使用數(shù)組時(shí)使用泛型的全部的約束,這僅僅是一個(gè)創(chuàng)建一個(gè)參數(shù)化類型數(shù)組的約束。我們將在學(xué)習(xí)如何寫泛型方法時(shí)再來討論這個(gè)話題。
類型參數(shù)通配符
假設(shè)我們需要寫一個(gè)方法來顯示一個(gè)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是一個(gè)泛型類型,如果我們?cè)噲D編譯這個(gè)方法,我們將會(huì)得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個(gè)方法:
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());
}
}
這段代碼能夠編譯通過同時(shí)不會(huì)有警告,但是它并不是非常地有效,因?yàn)橹挥心切┍宦暶鳛長(zhǎng)ist<Object>的list才會(huì)被允許使用這個(gè)方法。還記得么,類似于List<String>和List<Integer>這樣的List并不能被轉(zhuǎn)型為L(zhǎng)ist<Object>。事實(shí)上我們需要一個(gè)類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關(guān)心它被參數(shù)化為什么。解決辦法是使用類型參數(shù)通配符。方法可以被修改成這樣:
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());
}
}
這個(gè)版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。通配符“?”表示一個(gè)未知類型,類型List<?>被讀作“List of unknown”
作為一般原則,如果類型是泛型的,同時(shí)您并不知道或者并不關(guān)心值的類型,您應(yīng)該使用“?”通配符來代替一個(gè)未經(jīng)處理的類型。未經(jīng)處理的類型被允許僅是為了向下兼容,而且應(yīng)該只能夠被允許出現(xiàn)在老的代碼中。注意,無論如何,您不能在調(diào)用構(gòu)造器時(shí)使用通配符。下面的代碼是非法的:
List<?> l = new ArrayList<?>();
創(chuàng)建一個(gè)不知道類型的List是毫無道理的。如果您創(chuàng)建了它,那么您必須知道它將保持的元素是什么類型的。您可以在隨后的方法中不關(guān)心元素類型而去遍歷這里list,但是您需要在您創(chuàng)建它的時(shí)候描述元素的類型。如果你確實(shí)需要一個(gè)List來保持任何類型,那么您只能這么寫:
List<Object> l = new ArrayList<Object>();
從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個(gè)未經(jīng)處理的List。一個(gè)使用通配符的List<?>有兩個(gè)重要的特性。第一,考察類似于get()的方法,他們被聲明返回一個(gè)值,這個(gè)值的類型是類型參數(shù)中指定的。在這個(gè)例子中,類型是“unknown”,所以這些方法返回一個(gè)Object。既然我們期望的是調(diào)用這個(gè)object的toString()方法,程序能夠很好的滿足我們的意愿。
第二,考察List的類似add()的方法,他們被聲明為接受一個(gè)參數(shù),這個(gè)參數(shù)被類型參數(shù)所定義。出人意料的是,當(dāng)類型參數(shù)是未確定的,編譯器不允許您調(diào)用任何有不確定參數(shù)類型的方法——因?yàn)樗荒艽_認(rèn)您傳入了一個(gè)恰當(dāng)?shù)闹怠R粋€(gè)List(?)實(shí)際上是只讀的——既然編譯器不允許我們調(diào)用類似于add(),set(),addAll()這類的方法。
界定通配符
讓我們?cè)谖覀冊(cè)瓉淼睦由献餍┬⌒〉纳晕?fù)雜一點(diǎn)的改動(dòng)。假設(shè)我們希望寫一個(gè)sumList()方法來計(jì)算list中Number類型的值的合計(jì)。在以前,我們使用未經(jīng)處理的List,但是我們不想放棄類型安全,同時(shí)不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能調(diào)用List<Integer>、List<Double>中的方法了,而事實(shí)上我們需要調(diào)用。如果我們使用通配符,那么我們實(shí)際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調(diào)用,Number?還是Number的子類?甚至,String?這樣的一個(gè)方法也許會(huì)被寫成這樣:
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;
}
要修改這個(gè)方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數(shù)是未知的,但又是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”。理解這點(diǎn)非常重要,在這段文字中,Number被認(rèn)為是其自身的子類。
注意,這樣的話,那些類型轉(zhuǎn)換已經(jīng)不再需要了。我們并不知道list中元素的具體類型,但是我們知道他們能夠向上轉(zhuǎn)型為Number,因此我們可以把他們從list中把他們當(dāng)作一個(gè)Number對(duì)象取出。使用一個(gè)for/in循環(huán)能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當(dāng)您使用一個(gè)界定通配符時(shí),類似于List中的get()方法的那些方法將返回一個(gè)類型為上界的值。因此如果我們?cè)趂or/in循環(huán)中調(diào)用list.get(),我們將得到一個(gè)Number。在前一節(jié)說到使用通配符時(shí)類似于list.add()這種方法中的限制依然有效:舉個(gè)例子來說,如果編譯器允許我們調(diào)用這類方法,我們就可以將一個(gè)Integer放到一個(gè)聲明為僅保持Short值的list中去。
同樣可行的是使用下界通配符,不同的是用super替換extends。這個(gè)技巧在被調(diào)用的方法上有一點(diǎn)不同的作用。在實(shí)際應(yīng)用中,下界通配符要比上界通配符用得少。我們將在后面的章節(jié)里討論這個(gè)問題。
腳注
[1] 在本章中,我會(huì)堅(jiān)持用術(shù)語”泛型類型”來指一個(gè)聲明一個(gè)或多個(gè)類型變量的類型,用”參數(shù)化的類型”來指由實(shí)際類型參數(shù)來替換其類型變量的泛型類型。然而,在一般情況下,這種區(qū)別并不明顯,并且這些術(shù)語有時(shí)通用。
[2] 在撰寫本文時(shí)候,javac并不支持@SuppressWarnings 的注解。期望在Java 5.1中得到支持。
[3] 本節(jié)所示的3個(gè)printList()方法忽略了這樣一個(gè)事實(shí),即java.util 中List的所有實(shí)現(xiàn)類都有一個(gè)可用的toString()方法。還要注意這些方法假定List實(shí)現(xiàn)RandomAccess并在LinkedList實(shí)例中只提供了很差的運(yùn)行效率。
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 公司發(fā)布的 JDK 5.0 中的一個(gè)重要特性,它的最大優(yōu)點(diǎn)是提供了程序的類型安全同可以向后兼容。為了幫助讀者更好地理解和使用泛型,本文通過一些示例從基本原理,重要概念,關(guān)鍵技術(shù),以及相似技術(shù)比較等多個(gè)角度對(duì) Java 語言中的泛型技術(shù)進(jìn)行了介紹,重點(diǎn)強(qiáng)調(diào)了泛型中的一些基本但又不是很好理解的概念。
為了避免和 C++ 中的模板混淆,本文簡(jiǎn)要介紹了 Java 中的泛型和 C++ 中的模板的主要區(qū)別,希望這種比較能夠幫助讀者加深對(duì)泛型的理解。
引言
很多 Java 程序員都使用過集合(Collection),集合中元素的類型是多種多樣的,例如,有些集合中的元素是 Byte 類型的,而有些則可能是 String 類型的,等等。Java 語言之所以支持這么多種類的集合,是因?yàn)樗试S程序員構(gòu)建一個(gè)元素類型為 Object 的 Collection,所以其中的元素可以是任何類型。
當(dāng)使用 Collection 時(shí),我們經(jīng)常要做的一件事情就是要進(jìn)行類型轉(zhuǎn)換,當(dāng)轉(zhuǎn)換成所需的類型以后,再對(duì)它們進(jìn)行處理。很明顯,這種設(shè)計(jì)給編程人員帶來了極大的不便,同時(shí)也容易引入錯(cuò)誤。
在很多 Java 應(yīng)用中,上述情況非常普遍,為了解決這個(gè)問題,使 Java 語言變得更加安全好用,近些年的一些編譯器對(duì) Java 語言進(jìn)行了擴(kuò)充,使 Java 語言支持了"泛型",特別是 Sun 公司發(fā)布的 JDK 5.0 更是將泛型作為其中一個(gè)重要的特性加以推廣。
本文首先對(duì)泛型的基本概念和特點(diǎn)進(jìn)行簡(jiǎn)單介紹,然后通過引入幾個(gè)實(shí)例來討論帶有泛型的類,泛型中的子類型,以及范化方法和受限類型參數(shù)等重要概念。為了幫助讀者更加深刻的理解并使用泛型,本文還介紹了泛型的轉(zhuǎn)化,即,如何將帶有泛型的 Java 程序轉(zhuǎn)化成一般的沒有泛型的 Java 程序。這樣,讀者對(duì)泛型的理解就不會(huì)僅僅局限在表面上了。考慮到多數(shù)讀者僅僅是使用泛型,因此本文并未介紹泛型在編譯器中的具體實(shí)現(xiàn)。Java 中的泛型和 C++ 中的模板表面上非常相似,但實(shí)際上二者還是有很大區(qū)別的,本文最后簡(jiǎn)單介紹了 Java 中的泛型與 C++ 模板的主要區(qū)別。
泛型概覽
泛型本質(zhì)上是提供類型的"類型參數(shù)",它們也被稱為參數(shù)化類型(parameterized type)或參量多態(tài)(parametric polymorphism)。其實(shí)泛型思想并不是 Java 最先引入的,C++ 中的模板就是一個(gè)運(yùn)用泛型的例子。
GJ(Generic Java)是對(duì) Java 語言的一種擴(kuò)展,是一種帶有參數(shù)化類型的 Java 語言。用 GJ 編寫的程序看起來和普通的 Java 程序基本相同,只不過多了一些參數(shù)化的類型同時(shí)少了一些類型轉(zhuǎn)換。實(shí)際上,這些 GJ 程序也是首先被轉(zhuǎn)化成一般的不帶泛型的 Java 程序后再進(jìn)行處理的,編譯器自動(dòng)完成了從 Generic Java 到普通 Java 的翻譯。具體的轉(zhuǎn)化過程大致分為以下幾個(gè)部分:
- 將參數(shù)化類型中的類型參數(shù)"擦除"(erasure)掉;
- 將類型變量用"上限(upper bound)"取代,通常情況下這些上限是 Object。這里的類型變量是指實(shí)例域,本地方法域,方法參數(shù)以及方法返回值中用來標(biāo)記類型信息的"變量",例如:實(shí)例域中的變量聲明
A elem;
,方法聲明 Node (A elem){};
,其中,A 用來標(biāo)記 elem 的類型,它就是類型變量。
- 添加類型轉(zhuǎn)換并插入"橋方法"(bridge method),以便覆蓋(overridden)可以正常的工作。
轉(zhuǎn)化后的程序和沒有引入泛型時(shí)程序員不得不手工完成轉(zhuǎn)換的程序是非常一致的,具體的轉(zhuǎn)化過程會(huì)在后面介紹。GJ 保持了和 Java 語言以及 Java 虛擬機(jī)很好的兼容性,下面對(duì) GJ 的特點(diǎn)做一個(gè)簡(jiǎn)要的總結(jié)。
- 類型安全。 泛型的一個(gè)主要目標(biāo)就是提高 Java 程序的類型安全。使用泛型可以使編譯器知道變量的類型限制,進(jìn)而可以在更高程度上驗(yàn)證類型假設(shè)。如果沒有泛型,那么類型的安全性主要由程序員來把握,這顯然不如帶有泛型的程序安全性高。
- 消除強(qiáng)制類型轉(zhuǎn)換。泛型可以消除源代碼中的許多強(qiáng)制類型轉(zhuǎn)換,這樣可以使代碼更加可讀,并減少出錯(cuò)的機(jī)會(huì)。
- 向后兼容。支持泛型的 Java 編譯器(例如 JDK5.0 中的 Javac)可以用來編譯經(jīng)過泛型擴(kuò)充的 Java 程序(GJ 程序),但是現(xiàn)有的沒有使用泛型擴(kuò)充的 Java 程序仍然可以用這些編譯器來編譯。
- 層次清晰,恪守規(guī)范。無論被編譯的源程序是否使用泛型擴(kuò)充,編譯生成的字節(jié)碼均可被虛擬機(jī)接受并執(zhí)行。也就是說不管編譯器的輸入是 GJ 程序,還是一般的 Java 程序,經(jīng)過編譯后的字節(jié)碼都嚴(yán)格遵循《Java 虛擬機(jī)規(guī)范》中對(duì)字節(jié)碼的要求。可見,泛型主要是在編譯器層面實(shí)現(xiàn)的,它對(duì)于 Java 虛擬機(jī)是透明的。
- 性能收益。目前來講,用 GJ 編寫的代碼和一般的 Java 代碼在效率上是非常接近的。 但是由于泛型會(huì)給 Java 編譯器和虛擬機(jī)帶來更多的類型信息,因此利用這些信息對(duì) Java 程序做進(jìn)一步優(yōu)化將成為可能。
以上是泛型的一些主要特點(diǎn),下面通過幾個(gè)相關(guān)的例子來對(duì) Java 語言中的泛型進(jìn)行說明。
帶有泛型的類
為了幫助大家更好地理解 Java 語言中的泛型,我們?cè)谶@里先來對(duì)比兩段實(shí)現(xiàn)相同功能的 GJ 代碼和 Java 代碼。通過觀察它們的不同點(diǎn)來對(duì) Java 中的泛型有個(gè)總體的把握,首先來分析一下不帶泛型的 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
提供了兩個(gè)方法,即添加元素的方法 add(Object x)
,見第 2 行,以及返回該 Collection
的 Iterator
實(shí)例的方法 iterator()
,見第 3 行。Iterator
接口也提供了兩個(gè)方法,其一就是判斷是否有下一個(gè)元素的方法 hasNext()
,見第 8 行,另外就是返回下一個(gè)元素的方法 next()
,見第 7 行。LinkedList
類是對(duì)接口 Collection
的實(shí)現(xiàn),它是一個(gè)含有一系列節(jié)點(diǎn)的鏈表,節(jié)點(diǎn)中的數(shù)據(jù)類型是 Object,這樣就可以創(chuàng)建任意類型的節(jié)點(diǎn)了,比如 Byte, String 等等。上面這段程序就是用沒有泛型的傳統(tǒng)的 Java 語言編寫的代碼。接下來我們分析一下傳統(tǒng)的 Java 語言是如何使用這個(gè)類的。
代碼如下:
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 }
|
從上面的程序我們可以看出,當(dāng)從一個(gè)鏈表中提取元素時(shí)需要進(jìn)行類型轉(zhuǎn)換,這些都要由程序員顯式地完成。如果我們不小心從 String 類型的鏈表中試圖提取一個(gè) Byte 型的元素,見第 15 到第 16 行的代碼,那么這將會(huì)拋出一個(gè)運(yùn)行時(shí)的異常。請(qǐng)注意,上面這段程序可以順利地經(jīng)過編譯,不會(huì)產(chǎn)生任何編譯時(shí)的錯(cuò)誤,因?yàn)榫幾g器并不做類型檢查,這種檢查是在運(yùn)行時(shí)進(jìn)行的。不難發(fā)現(xiàn),傳統(tǒng) Java 語言的這一缺陷推遲了發(fā)現(xiàn)程序中錯(cuò)誤的時(shí)間,從軟件工程的角度來看,這對(duì)軟件的開發(fā)是非常不利的。接下來,我們討論一下如何用 GJ 來實(shí)現(xiàn)同樣功能的程序。源程序如下:
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 }
|
程序的功能并沒有任何改變,只是在實(shí)現(xiàn)方式上使用了泛型技術(shù)。我們注意到上面程序的接口和類均帶有一個(gè)類型參數(shù) A,它被包含在一對(duì)尖括號(hào)(< >)中,見第 1,6 和 13 行,這種表示法遵循了 C++ 中模板的表示習(xí)慣。這部分程序和上面程序的主要區(qū)別就是在 Collection
, Iterator
, 或 LinkedList
出現(xiàn)的地方均用 Collection<A>
, Iterator<A>
, 或 LinkedList<A>
來代替,當(dāng)然,第 22 行對(duì)構(gòu)造函數(shù)的聲明除外。
下面再來分析一下在 GJ 中是如何對(duì)這個(gè)類進(jìn)行操作的,程序如下:
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 }
|
在這里我們可以看到,有了泛型以后,程序員并不需要進(jìn)行顯式的類型轉(zhuǎn)換,只要賦予一個(gè)參數(shù)化的類型即可,見第 4,8 和 12 行,這是非常方便的,同時(shí)也不會(huì)因?yàn)橥涍M(jìn)行類型轉(zhuǎn)換而產(chǎn)生錯(cuò)誤。另外需要注意的就是當(dāng)試圖從一個(gè)字符串類型的鏈表里提取出一個(gè)元素,然后將它賦值給一個(gè) Byte 型的變量時(shí),見第 16 行,編譯器將會(huì)在編譯時(shí)報(bào)出錯(cuò)誤,而不是由虛擬機(jī)在運(yùn)行時(shí)報(bào)錯(cuò),這是因?yàn)榫幾g器會(huì)在編譯時(shí)刻對(duì) GJ 代碼進(jìn)行類型檢查,此種機(jī)制有利于盡早地發(fā)現(xiàn)并改正錯(cuò)誤。
類型參數(shù)的作用域是定義這個(gè)類型參數(shù)的整個(gè)類,但是不包括靜態(tài)成員函數(shù)。這是因?yàn)楫?dāng)訪問同一個(gè)靜態(tài)成員函數(shù)時(shí),同一個(gè)類的不同實(shí)例可能有不同的類型參數(shù),所以上述提到的那個(gè)作用域不應(yīng)該包括這些靜態(tài)函數(shù),否則就會(huì)引起混亂。
泛型中的子類型
在 Java 語言中,我們可以將某種類型的變量賦值給其父類型所對(duì)應(yīng)的變量,例如,String 是 Object 的子類型,因此,我們可以將 String 類型的變量賦值給 Object 類型的變量,甚至可以將 String [ ] 類型的變量(數(shù)組)賦值給 Object [ ] 類型的變量,即 String [ ] 是 Object [ ] 的子類型。
上述情形恐怕已經(jīng)深深地印在了廣大讀者的腦中,對(duì)于泛型來講,上述情形有所變化,因此請(qǐng)廣大讀者務(wù)必引起注意。為了說明這種不同,我們還是先來分析一個(gè)小例子,代碼如下所示:
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>
,按照以往的經(jīng)驗(yàn),這種賦值好像是正確的,因?yàn)?List<String>
應(yīng)該是 List<Object>
的子類型。這里需要特別注意的是,這種賦值在泛型當(dāng)中是不允許的!List<String>
也不是 List<Object>
的子類型。
如果上述賦值是合理的,那么上面代碼的第三行的操作將是可行的,因?yàn)?lo
是 List<Object>
,所以向其添加 Integer 類型的元素應(yīng)該是完全合法的。讀到此處,我們已經(jīng)看到了第二行的這種賦值所潛在的危險(xiǎn),它破壞了泛型所帶來的類型安全性。
一般情況下,如果 A 是 B 的子類型,C 是某個(gè)泛型的聲明,那么 C<A>
并不是 C<B>
的子類型,我們也不能將 C<A>
類型的變量賦值給 C<B>
類型的變量。這一點(diǎn)和我們以前接觸的父子類型關(guān)系有很大的出入,因此請(qǐng)讀者務(wù)必引起注意。
泛化方法和受限類型參數(shù)
在這一部分我們將討論有關(guān)泛化方法(generic method )和受限類型參數(shù)(bounded type parameter)的內(nèi)容,這是泛型中的兩個(gè)重要概念,還是先來分析一下與此相關(guān)的代碼。
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 }
|
這里定義了一個(gè)接口 Comparable<A>
,用來和 A 類型的對(duì)象進(jìn)行比較。類 Byte 實(shí)現(xiàn)了這個(gè)接口,并以它自己作為類型參數(shù),因此,它們自己就可以和自己進(jìn)行比較了。
第 14 行到第 25 行的代碼定義了一個(gè)類 Collections
,這個(gè)類包含一個(gè)靜態(tài)方法 max(Collection<A> xs)
,它用來在一個(gè)非空的 Collection
中尋找最大的元素并返回這個(gè)元素。這個(gè)方法的兩個(gè)特點(diǎn)就是它是一個(gè)泛化方法并且有一個(gè)受限類型參數(shù)。
之所以說它是泛化了的方法,是因?yàn)檫@個(gè)方法可以應(yīng)用到很多種類型上。當(dāng)要將一個(gè)方法聲明為泛化方法時(shí),我們只需要在這個(gè)方法的返回類型(A)之前加上一個(gè)類型參數(shù)(A),并用尖括號(hào)(< >)將它括起來。這里的類型參數(shù)(A)是在方法被調(diào)用時(shí)自動(dòng)實(shí)例化的。例如,假設(shè)對(duì)象 m 的類型是 Collection<Byte>
,那么當(dāng)使用下面的語句:
Byte x = Collections.max(m);
|
調(diào)用方法 max 時(shí),該方法的參數(shù) A 將被推測(cè)為 Byte。
根據(jù)上面討論的內(nèi)容,泛化方法 max 的完整聲明應(yīng)該是下面的形式:
< A > A max (Collection<A> xs) {
max 的方法體
}
|
但是,我們見到的 max 在 < A > 中還多了 "implements Comparable<A>" 一項(xiàng),這是什么呢?這就是我們下面將要談到的"受限的類型參數(shù)"。在上面的例子中,類型參數(shù) A 就是一個(gè)受限的的類型參數(shù),因?yàn)樗皇欠褐溉魏晤愋停侵改切┳约汉妥约鹤鞅容^的類型。例如參數(shù)可以被實(shí)例化為 Byte,因?yàn)槌绦蛑杏?Byte implements Comparable<Byte>
的語句,參見第 5 行。這種限制(或者說是范圍)通過如下的方式表示,"類型參數(shù) implements 接口",或是 "類型參數(shù) extend 類",上面程序中的"Byte implements Comparable<Byte>"就是一例。
泛型的轉(zhuǎn)化
在前面的幾部分內(nèi)容當(dāng)中,我們介紹了有關(guān)泛型的基礎(chǔ)知識(shí),到此讀者對(duì) Java 中的泛型技術(shù)應(yīng)該有了一定的了解,接下來的這部分內(nèi)容將討論有關(guān)泛型的轉(zhuǎn)化,即如何將帶有泛型的 Java 代碼轉(zhuǎn)化成一般的沒有泛型 Java 代碼。其實(shí)在前面的部分里,我們或多或少地也提到了一些相關(guān)的內(nèi)容,下面再來詳細(xì)地介紹一下。
首先需要明確的一點(diǎn)是上面所講的這種轉(zhuǎn)化過程是由編譯器(例如:Javac)完成的,虛擬機(jī)并不負(fù)責(zé)完成這一任務(wù)。當(dāng)編譯器對(duì)帶有泛型的 Java 代碼進(jìn)行編譯時(shí),它會(huì)去執(zhí)行類型檢查和類型推斷,然后生成普通的不帶泛型的字節(jié)碼,這種字節(jié)碼可以被一般的Java虛擬機(jī)接收并執(zhí)行,這種技術(shù)被稱為擦除(erasure)。
可見,編譯器可以在對(duì)源程序(帶有泛型的 Java 代碼)進(jìn)行編譯時(shí)使用泛型類型信息保證類型安全,對(duì)大量如果沒有泛型就不會(huì)去驗(yàn)證的類型安全約束進(jìn)行驗(yàn)證,同時(shí)在生成的字節(jié)碼當(dāng)中,將這些類型信息清除掉。
對(duì)于不同的情況,擦除技術(shù)所執(zhí)行的"擦除"動(dòng)作是不同的,主要分為以下幾種情況:
- 對(duì)于參數(shù)化類型,需要?jiǎng)h除其中的類型參數(shù),例如,
LinkedList<A>
將被"擦除"為 LinkedList;
- 對(duì)于非參數(shù)化類型,不作擦除,或者說用它自己來擦除自己,例如 String 將被"擦除"為 String;
- 對(duì)于類型變量(有關(guān)類型變量的說明請(qǐng)參考"泛型概覽"相關(guān)內(nèi)容),要用它們的上限來對(duì)它們進(jìn)行替換。多數(shù)情況下這些上限是 Object,但是也有例外,后面的部分將會(huì)對(duì)此進(jìn)行介紹。
除此之外,還需要注意的一點(diǎn)是,在某些情況下,擦除技術(shù)需要引入類型轉(zhuǎn)換(cast),這些情況主要包括:
情況 1. 方法的返回類型是類型參數(shù);
情況 2. 在訪問數(shù)據(jù)域時(shí),域的類型是一個(gè)類型參數(shù)。
例如在本文"帶有泛型的類"一小節(jié)的最后,我們給出了一段測(cè)試程序,一個(gè) Test 類。這個(gè)類包含以下幾行代碼:
8 LinkedList<String> ys = new LinkedList<String>();
9 ys.add("zero"); ys.add("one");
10 String y = ys.iterator().next();
這部分代碼轉(zhuǎn)換后就變成了如下的代碼:
8 LinkedList ys = new LinkedList();
9 ys.add("zero"); ys.add("one");
10 String y = (String)ys.iterator().next();
|
第 10 行的代碼進(jìn)行了類型轉(zhuǎn)換,這是因?yàn)樵谡{(diào)用 next()
方法時(shí),編譯器發(fā)現(xiàn)該方法的返回值類型是類型參數(shù) A(請(qǐng)參見對(duì)方法 next()
的定義),因此根據(jù)上面提到的情況 1,需要進(jìn)行類型轉(zhuǎn)換。
上面介紹了泛型轉(zhuǎn)化中的擦除技術(shù),接下來,我們討論一下泛型轉(zhuǎn)化中的另外一個(gè)重要問題--橋方法(bridge method)。
Java 是一種面向?qū)ο蟮恼Z言,因此覆蓋(overridden)是其中的一項(xiàng)重要技術(shù)。覆蓋能夠正常"工作"的前提是方法名和方法的參數(shù)類型及個(gè)數(shù)完全匹配(參數(shù)的順序也應(yīng)一致),為了滿足這項(xiàng)要求,編譯器在泛型轉(zhuǎn)化中引入了橋方法(bridge method)。接下來,我們通過一個(gè)例子來分析一下橋方法在泛型轉(zhuǎn)化中所起的作用。在本文"泛化方法和受限類型參數(shù)"一小節(jié)所給出的代碼中,第 9 行到第 11 行的程序如下所示:
9 public int compareTo(Byte that) {
10 return this.value - that.value;
11 }
這部分代碼經(jīng)過轉(zhuǎn)化,就變成了下面的樣子:
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)
就是一個(gè)橋方法,在這里引入這個(gè)方法是為了保證覆蓋能夠正常的發(fā)生。我們?cè)谇懊嫣岬竭^,覆蓋必須保證方法名和參數(shù)的類型及數(shù)目完全匹配,在這里通過引入這個(gè)"橋"即可達(dá)到這一目的,由這個(gè)"橋"進(jìn)行類型轉(zhuǎn)換,并調(diào)用第 9 行參數(shù)類型為 Byte 的方法 compareTo(Byte that),需要注意的一點(diǎn)是這里的 "Object" 也并不一定是完全匹配的類型,但由于它是 Java 語言中類層次結(jié)構(gòu)的根,所以這里用 "Object" 可以接受其他任何類型的參數(shù)。
根據(jù)面向?qū)ο蟮幕靖拍睿覀冎溃剌d(overloading)允許橋方法和原來的方法共享同一個(gè)方法名,正如上面例子所顯示的那樣,因此橋方法的引入是完全合法的。一般情況下,當(dāng)一個(gè)類實(shí)現(xiàn)了一個(gè)參數(shù)化的接口或是繼承了一個(gè)參數(shù)化的類時(shí),需要引入橋方法。
到此,我們對(duì)泛型中的子類型,帶有泛型的類,泛化方法,受限類型參數(shù)以及泛型的轉(zhuǎn)化進(jìn)行了簡(jiǎn)要的介紹,下面部分將結(jié)合這些技術(shù)對(duì)前面提到的例子進(jìn)行一下總結(jié),以便能夠幫助讀者更深刻更全面地理解泛型。
首先來分析一下本文提到的那個(gè) Collection
的例子。這里先是定義了兩個(gè)接口 Collection
和 Iterator
,然后又定義了一個(gè)對(duì)接口 Collection
的一個(gè)實(shí)現(xiàn) LinkedList
。根據(jù)上面所介紹的對(duì)泛型的轉(zhuǎn)化過程,這段代碼轉(zhuǎn)化后的 Java 程序?yàn)椋?/p>
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 }
|
通過分析上述代碼,我們不難發(fā)現(xiàn),所有參數(shù)化類型 Collection, Iterator 和 LinkedList 中的類型參數(shù) "A" 全都被擦除了。另外,剩下的類型變量 "A" 都用其上限進(jìn)行了替換,這里的上限是 Object,見黑體字標(biāo)出的部分,這是轉(zhuǎn)化的關(guān)鍵部分。
下面我們分析一下在介紹有關(guān)泛化方法(generic method)和受限類型參數(shù)(bounded type parameter)時(shí)舉的那個(gè)例子,該段 GJ 代碼經(jīng)過轉(zhuǎn)換后的等價(jià) 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 }
|
同樣請(qǐng)讀者注意黑體字標(biāo)出的部分,這些關(guān)鍵點(diǎn)我們?cè)谇懊嬉呀?jīng)介紹過了,故不贅述。唯一需要注意的一點(diǎn)就是第 18,20,22 行出現(xiàn)的Comparable。在泛型轉(zhuǎn)化中,類型變量應(yīng)該用其上限來替換,一般情況下這些上限是 "Object",但是當(dāng)遇到受限的類型參數(shù)時(shí),這個(gè)上限就不再是 "Object" 了,編譯器會(huì)用限制這些類型參數(shù)的類型來替換它,上述代碼就用了對(duì) A 進(jìn)行限制的類型 "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 }
|
根據(jù)以上所講的內(nèi)容,這部分代碼轉(zhuǎn)換后的 Java 程序應(yīng)該是如下這個(gè)樣子:
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 }
|
相信有些讀者已經(jīng)發(fā)現(xiàn)了這里的問題,這不是一段合法的 Java 源程序,因?yàn)榈?14 行和第 16 行的兩個(gè) next() 有相同的參數(shù),無法加以區(qū)分。代碼中的 %1% 和 %2% 是為了區(qū)分而人為加入的,并非 GJ 轉(zhuǎn)化的結(jié)果。
不過,這并不是什么太大的問題,因?yàn)?Java 虛擬機(jī)可以區(qū)分這兩個(gè) next() 方法,也就是說,從 Java 源程序的角度來看,上述程序是不正確的,但是當(dāng)編譯成字節(jié)碼時(shí),JVM 可以對(duì)兩個(gè) next() 方法進(jìn)行識(shí)別。這是因?yàn)椋?JVM 中,方法定義時(shí)所使用的方法簽名包括方法的返回類型,這樣一來,只要 GJ 編譯出的字節(jié)碼符合Java字節(jié)碼的規(guī)范即可,這也正好說明了 GJ 和 JVM 中字節(jié)碼規(guī)范要求的一致性!
最后,值得一提的是,JDK 5.0 除了在編譯器層面對(duì) Java 中的泛型進(jìn)行了支持,Java 的類庫為支持泛型也做了相應(yīng)地調(diào)整,例如,集合框架中所有的標(biāo)準(zhǔn)集合接口都進(jìn)行了泛型化,同時(shí),集合接口的實(shí)現(xiàn)也都進(jìn)行了相應(yīng)地泛型化。
Java 中的泛型與 C++ 模板的比較
GJ 程序的語法在表面上與 C++ 中的模板非常類似,但是二者之間有著本質(zhì)的區(qū)別。
首先,Java 語言中的泛型不能接受基本類型作為類型參數(shù)――它只能接受引用類型。這意味著可以定義 List<Integer>,但是不可以定義 List<int>。
其次,在 C++ 模板中,編譯器使用提供的類型參數(shù)來擴(kuò)充模板,因此,為 List<A> 生成的 C++ 代碼不同于為 List<B> 生成的代碼,List<A> 和 List<B> 實(shí)際上是兩個(gè)不同的類。而 Java 中的泛型則以不同的方式實(shí)現(xiàn),編譯器僅僅對(duì)這些類型參數(shù)進(jìn)行擦除和替換。類型 ArrayList<Integer> 和 ArrayList<String> 的對(duì)象共享相同的類,并且只存在一個(gè) ArrayList 類。
總結(jié)
本文通過一些示例從基本原理,重要概念,關(guān)鍵技術(shù),以及相似技術(shù)比較等多個(gè)角度對(duì) Java 語言中的泛型技術(shù)進(jìn)行了介紹,希望這種介紹方法能夠幫助讀者更好地理解和使用泛型。本文主要針對(duì)廣大的 Java 語言使用者,在介紹了泛型的基本概念后,重點(diǎn)介紹了比較底層的泛型轉(zhuǎn)化技術(shù),旨在幫助讀者更加深刻地掌握泛型,筆者相信這部分內(nèi)容可以使讀者避免對(duì)泛型理解的表面化,也所謂知其然更知其所以然。
參考資料
關(guān)于作者
 |
|
 |
周晶,2006年4月畢業(yè)于北京航空航天大學(xué)計(jì)算機(jī)學(xué)院,獲計(jì)算機(jī)碩士學(xué)位。主要研究領(lǐng)域?yàn)楦呒?jí)編譯技術(shù),Java虛擬機(jī)技術(shù)。 beyond.zhou@gmail.com
|