級別: 初級 周 晶 ([email protected]), 計算機碩士 2006 年 12 月 28 日 泛型是 Sun 公司發佈的 JDK 5.0 中的一個重要特性,它的最大優點是提供了程序的類型安全同可以向後兼容。爲了幫助讀者更好地理解和使用泛型,本文通過一些示例從基本原理,重要概念,關鍵技術,以及相似技術比較等多個角度對 Java 語言中的泛型技術進行了介紹,重點強調了泛型中的一些基本但又不是很好理解的概 很多 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 的翻譯。具體的轉化過程大致分爲以下幾個部分:
轉化後的程序和沒有引入泛型時程序員不得不手工完成轉換的程序是非常一致的,具體的轉化過程會在後面介紹。GJ 保持了和 Java 語言以及 Java 虛擬機很好的兼容性,下面對 GJ 的特點做一個簡要的總結。
以上是泛型的一些主要特點,下面通過幾個相關的例子來對 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; 38 } else throw new NoSuchElementException (); 39 } 40 }; 41 } 42 } 接口 代碼如下: 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 } 從上面的程序我們可以看出,當從一個鏈表中提取元素時需要進行類型轉換,這些都要由程序員顯式地完成。如果我們不小心從 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; 36 } else throw new NoSuchElementException (); 37 } 38 }; 39 } 40 } 程序的功能並沒有任何改變,只是在實現方式上使用了泛型技術。我們注意到上面程序的接口和類均帶有一個類型參數 A,它被包含在一對尖括號(< >)中,見第 1,6 和 13 行,這種表示法遵循了 C++ 中模板的表示習慣。這部分程序和上面程序的主要區別就是在 下面再來分析一下在 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); 上述代碼的第二行將 如果上述賦值是合理的,那麼上面代碼的第三行的操作將是可行的,因爲 一般情況下,如果 A 是 B 的子類型,C 是某個泛型的聲明,那麼 在這一部分我們將討論有關泛化方法(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 } 這裏定義了一個接口 第 14 行到第 25 行的代碼定義了一個類 之所以說它是泛化了的方法,是因爲這個方法可以應用到很多種類型上。當要將一個方法聲明爲泛化方法時,我們只需要在這個方法的返回類型(A)之前加上一個類型參數(A),並用尖括號(< >)將它括起來。這裏的類型參數(A)是在方法被調用時自動實例化的。例如,假設對象 m 的類型是 Byte x = Collections.max(m); 調用方法 max 時,該方法的參數 A 將被推測爲 Byte。 根據上面討論的內容,泛化方法 max 的完整聲明應該是下面的形式: < A > A max (Collection<A> xs) { max 的方法體 }
類型參數 A 就是一個受限的的類型參數,因爲它不是泛指任何類型,而是指那些自己和自己作比較的類型。例如參數可以被實例化爲 Byte,因爲程序中有 在前面的幾部分內容當中,我們介紹了有關泛型的基礎知識,到此讀者對 Java 中的泛型技術應該有了一定的瞭解,接下來的這部分內容將討論有關泛型的轉化,即如何將帶有泛型的 Java 代碼轉化成一般的沒有泛型 Java 代碼。其實在前面的部分裏,我們或多或少地也提到了一些相關的內容,下面再來詳細地介紹一下。 首先需要明確的一點是上面所講的這種轉化過程是由編譯器(例如:Javac)完成的,虛擬機並不負責完成這一任務。當編譯器對帶有泛型的 Java 代碼進行編譯時,它會去執行類型檢查和類型推斷,然後生成普通的不帶泛型的字節碼,這種字節碼可以被一般的Java虛擬機接收並執行,這種技術被稱爲擦除(erasure)。 可見,編譯器可以在對源程序(帶有泛型的 Java 代碼)進行編譯時使用泛型類型信息保證類型安全,對大量如果沒有泛型就不會去驗證的類型安全約束進行驗證,同時在生成的字節碼當中,將這些類型信息清除掉。 對於不同的情況,擦除技術所執行的"擦除"動作是不同的,主要分爲以下幾種情況:
除此之外,還需要注意的一點是,在某些情況下,擦除技術需要引入類型轉換(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 行的代碼進行了類型轉換,這是因爲在調用 上面介紹了泛型轉化中的擦除技術,接下來,我們討論一下泛型轉化中的另外一個重要問題--橋方法(bridge method)。 Java 是一種面向對象的語言,因此覆蓋(overridden)是其中的一項重要技術。覆蓋能夠正常"工作"的前提是方法名和方法的參數類型及個數完全匹配(參數的順序也應一致),爲了滿足這項要求,編譯器在泛型轉化中引入了橋方法(bridge method)。接下來,我們通過一個例子來分析一下橋方法在泛型轉化中所起的作用。在本文"泛化方法和受限類型參數"一小節所給出的代碼中,第 9 行到第 11 行的程序如下所示: public int compareTo(Byte that) { return this.value - that.value; } 這部分代碼經過轉化,就變成了下面的樣子: public int compareTo(Byte that) { return this.value - that.value; } public int compareTo(Object that) { return this.compareTo((Byte)that); } 第 12 行的方法 根據面向對象的基本概念,我們知道,重載(overloading)允許橋方法和原來的方法共享同一個方法名,正如上面例子所顯示的那樣,因此橋方法的引入是完全合法的。一般情況下,當一個類實現了一個參數化的接口或是繼承了一個參數化的類時,需要引入橋方法。 到此,我們對泛型中的子類型,帶有泛型的類,泛化方法,受限類型參數以及泛型的轉化進行了簡要的介紹,下面部分將結合這些技術對前面提到的例子進行一下總結,以便能夠幫助讀者更深刻更全面地理解泛型。 首先來分析一下本文提到的那個 interface Collection { public void add (Object x); public Iterator iterator (); } interface Iterator { public Object next (); public boolean hasNext (); 9 } class NoSuchElementException extends RuntimeException {} class LinkedList implements Collection { protected class Node { Object elt; Node next = null; Node (Object elt) { this.elt = elt; } } protected Node head = null, tail = null; public LinkedList () {} public void add (Object elt) { if (head == null) { head = new Node(elt); tail = head; } else { tail.next = new Node(elt); tail = tail.next; } } public Iterator iterator () { return new Iterator () { protected Node ptr = head; public boolean hasNext () { return ptr != null; } public Object next () { if (ptr != null) { Object elt = ptr.elt; ptr = ptr.next; return elt; } else { throw new NoSuchElementException (); } } }; } } 通過分析上述代碼,我們不難發現,所有參數化類型 Collection, Iterator 和 LinkedList 中的類型參數 "A" 全都被擦除了。另外,剩下的類型變量 "A" 都用其上限進行了替換,這裏的上限是 Object,見黑體字標出的部分,這是轉化的關鍵部分。 下面我們分析一下在介紹有關泛化方法(generic method)和受限類型參數(bounded type parameter)時舉的那個例子,該段 GJ 代碼經過轉換後的等價 Java 程序如下所示: interface Comparable { public int compareTo(Object that); } class Byte implements Comparable { private byte value; public Byte(byte value) {this.value = value;} public byte byteValue(){return value;} public int compareTo(Byte that) { return this.value - that.value; } public int compareTo(Object that) { return this.compareTo((Byte)that); } } class Collections { public static Comparable max(Collection xs) { Iterator xi = xs.iterator(); Comparable w = (Comparable)xi.next(); while (xi.hasNext()) { Comparable x = (Comparable)xi.next(); if (w.compareTo(x) < 0) w = x; } return w; } } 同樣請讀者注意黑體字標出的部分,這些關鍵點我們在前面已經介紹過了,故不贅述。唯一需要注意的一點就是第 18,20,22 行出現的Comparable。在泛型轉化中,類型變量應該用其上限來替換,一般情況下這些上限是 "Object",但是當遇到受限的類型參數時,這個上限就不再是 "Object" 了,編譯器會用限制這些類型參數的類型來替換它,上述代碼就用了對 A 進行限制的類型 "Comparable" 來替換 A。 橋方法的引入,爲解決覆蓋問題帶來了方便,但是這種方法還存在一些問題,例如下面這段代碼: interface Iterator<A> { public boolean hasNext (); public A next (); } class Interval implements Iterator<Integer> { private int i; private int n; public Interval (int l, int u) { i = l; n = u; } public boolean hasNext () { return (i <= n); } public Integer next () { return new Integer(i++); } } 根據以上所講的內容,這部分代碼轉換後的 Java 程序應該是如下這個樣子: interface Iterator { public boolean hasNext (); public Object next (); } class Interval implements Iterator { private int i; private int n; public Interval (int l, int u) { i = l; n = u; } public boolean hasNext () { return (i <= n); } public Integer next%1% () { return new Integer(i++); } // bridge public Object next%2%() { return next%1%(); } } 相信有些讀者已經發現了這裏的問題,這不是一段合法的 Java 源程序,因爲第 14 行和第 16 行的兩個 next() 有相同的參數,無法加以區分。代碼中的 %1% 和 %2% 是爲了區分而人爲加入的,並非 GJ 轉化的結果。 不過,這並不是什麼太大的問題,因爲 Java 虛擬機可以區分這兩個 next() 方法,也就是說,從 Java 源程序的角度來看,上述程序是不正確的,但是當編譯成字節碼時,JVM 可以對兩個 next() 方法進行識別。這是因爲,在 JVM 中,方法定義時所使用的方法簽名包括方法的返回類型,這樣一來,只要 GJ 編譯出的字節碼符合Java字節碼的規範即可,這也正好說明了 GJ 和 JVM 中字節碼規範要求的一致性! 最後,值得一提的是,JDK 5.0 除了在編譯器層面對 Java 中的泛型進行了支持,Java 的類庫爲支持泛型也做了相應地調整,例如,集合框架中所有的標準集合接口都進行了泛型化,同時,集合接口的實現也都進行了相應地泛型化。 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 語言使用者,在介紹了泛型的基本概念後,重點介紹了比較底層的泛型轉化技術,旨在幫助讀者更加深刻地掌握泛型,筆者相信這部分內容可以使讀者避免對泛型理解的表面化,也所謂知其然更知其所以然。
原文鏈接:http://www.ibm.com/developerworks/cn/java/j-lo-gj/index.html?S_TACT=105AGX52&S_CMP=techcsdn |