2006 年 12 月 28 日 泛型是 Sun 公司發(fā)布的 JDK 5.0 中的一個(gè)重要特性,它的最大優(yōu)點(diǎn)是提供了程序的類型安全同可以向后兼容。為了幫助讀者更好地理解和使用泛型,本文通過一些示例從基本原理,重要概念,關(guān)鍵技術(shù),以及相似技術(shù)比較等多個(gè)角度對(duì) Java 語(yǔ)言中的泛型技術(shù)進(jìn)行了介紹,重點(diǎn)強(qiáng)調(diào)了泛型中的一些基本但又不是很好理解的概念。 為了避免和 C++ 中的模板混淆,本文簡(jiǎn)要介紹了 Java 中的泛型和 C++ 中的模板的主要區(qū)別,希望這種比較能夠幫助讀者加深對(duì)泛型的理解。 引言 很多 Java 程序員都使用過集合(Collection),集合中元素的類型是多種多樣的,例如,有些集合中的元素是 Byte 類型的,而有些則可能是 String 類型的,等等。Java 語(yǔ)言之所以支持這么多種類的集合,是因?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 語(yǔ)言變得更加安全好用,近些年的一些編譯器對(duì) Java 語(yǔ)言進(jìn)行了擴(kuò)充,使 Java 語(yǔ)言支持了"泛型",特別是 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 語(yǔ)言的一種擴(kuò)展,是一種帶有參數(shù)化類型的 Java 語(yǔ)言。用 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 語(yǔ)言以及 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é)碼的要求??梢姡盒椭饕窃诰幾g器層面實(shí)現(xiàn)的,它對(duì)于 Java 虛擬機(jī)是透明的。
- 性能收益。目前來講,用 GJ 編寫的代碼和一般的 Java 代碼在效率上是非常接近的。 但是由于泛型會(huì)給 Java 編譯器和虛擬機(jī)帶來更多的類型信息,因此利用這些信息對(duì) Java 程序做進(jìn)一步優(yōu)化將成為可能。
以上是泛型的一些主要特點(diǎn),下面通過幾個(gè)相關(guān)的例子來對(duì) Java 語(yǔ)言中的泛型進(jìn)行說明。
帶有泛型的類 為了幫助大家更好地理解 Java 語(yǔ)言中的泛型,我們?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; 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 語(yǔ)言編寫的代碼。接下來我們分析一下傳統(tǒng)的 Java 語(yǔ)言是如何使用這個(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(); 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 語(yǔ)言的這一缺陷推遲了發(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; 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 語(yǔ)言中,我們可以將某種類型的變量賦值給其父類型所對(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)使用下面的語(yǔ)句: 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> 的語(yǔ)句,參見第 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(yǔ)言,因此覆蓋(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 語(yǔ)言中類層次結(jié)構(gòu)的根,所以這里用 "Object" 可以接受其他任何類型的參數(shù)。 根據(jù)面向?qū)ο蟮幕靖拍?,我們知道,重載(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 的類庫(kù)為支持泛型也做了相應(yīng)地調(diào)整,例如,集合框架中所有的標(biāo)準(zhǔn)集合接口都進(jìn)行了泛型化,同時(shí),集合接口的實(shí)現(xiàn)也都進(jìn)行了相應(yīng)地泛型化。
Java 中的泛型與 C++ 模板的比較 GJ 程序的語(yǔ)法在表面上與 C++ 中的模板非常類似,但是二者之間有著本質(zhì)的區(qū)別。 首先,Java 語(yǔ)言中的泛型不能接受基本類型作為類型參數(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 語(yǔ)言中的泛型技術(shù)進(jìn)行了介紹,希望這種介紹方法能夠幫助讀者更好地理解和使用泛型。本文主要針對(duì)廣大的 Java 語(yǔ)言使用者,在介紹了泛型的基本概念后,重點(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 |
|