下面就是「泛型」能夠解決的問題。假設你希望運用 lists,有些人希望擁有 list of bytes,有些人希望擁有 list of strings,還有一些人希望擁有 lists of lists of strings。在 Java 語言中,這很簡單。你可以使用相同的 class 表達上述三個你所想要的東西,它們都擁有型別為 class Object 的元素,見表一(a)。
表一/ lists的使用 (a) Java (b) Generic Java
List 的型態 使用的 Class (a) list of bytes list of strings list of list of strings - List List List
(b) list of bytes list of strings list of list of strings - List<Byte> List<String> List<List<String>>
為了保持語言的簡單,你被迫自己動手做一些事情:是的,你必須記住一個事實,那就是你所擁有的是個 list of bytes(或 strings 或 lists),而後當你從中萃取出一個元素時,在更進一步處理之前,你必須將它轉型,從 class Object 轉為 class Byte(或 String 或 List)。Java2 的 collection class framework 就是以此方式對待各種容器類別(其中涵蓋 lists)。
愛因斯坦說過,每件事情都應該盡量簡化,但不要過度簡介(everything should be as simple as possible, but no simpler)。可能有人會說以上太過簡化了。假如我們以泛型型別(generic types)來擴充 Java 語言,就有可能以一種更直接的方法來表現 lists 的相關資訊;見表一(b)。於是,編譯器可以追蹤記錄你是否擁有一個 list of bytes(或 strings 或 lists),而你也就不再需要將型別轉回 class Byte(或 String 或 List<String>)。某種情況下,這很類似 Ada 語言的 generics 或 C++ 語言的 templates。它也很類似所謂的 functional languages(如 ML 和 Haskell)中的 parametric polymorphism。
Sun 的工作受到一些前人努力的影響,我指的是令人測目的 Generic Java (GJ)。GJ 最初由三組人馬設計,分別是 Sun 的 Gilad Bracha 和 David Stoutamire,瑞士聯邦科技學會的 Martin Odersky,和我本人。Bracha 如今帶領 Sun 的一個部門,正在為Java加入泛型特性而沖鋒陷陣。Odersky 和我則是在相關的專家委員會里頭。在這篇文章中,我要以一種前瞻的態度描述 GJ,看看 Sun 的努力可能浮現出什麼成果。GJ 的進一步細節,可自以下網站獲得:http://www.cs.bell-labs.com/~wadler/gj/。GJ 編譯器由 Odersky 撰寫,目前可由上述站點下載。這個 GJ 編譯器同時也是 Sun 目前服役的 Java 編譯器的基礎 ─ 雖然後者尚未支援泛型。GJ 和 GJ 編譯器并不是 Sun 的產品。
例一顯示 list interface 和 iterator interface(兩者都是根據 Java collections library 而做的高度簡化),以及一個將 list interface實作出來的 linked list class,和一個測試用的 class。這里的 list interface 提供了一個 add method,用來為 list 添加新元素,還有一個 iterator method,用來傳回 list 的一個迭代器。至於 iterator interface 則是提供了一個 hasNext method,用來決定迭代是否已經完成,以及一個 next method,用來取得下一個元素,并將迭代器推進一個位置。linked list class 實作出 list interface,但我略而不述其中細節。add method 接受一個物件,next method 則是傳回一個物件。由於每一個 class 都是Object 的 subclass,所以你可以以 Byte, String, List 自己,或任何其他 class 元素來組成 lists。
例一:List interface 和 iterator interface interface List { public void add (Object x); public Iterator iterator (); } interface Iterator { public Object next (); public boolean hasNext (); } class LinkedList implements List { ... } class Test { public static void main (String[] args) { // byte list List xs = new LinkedList(); xs.add(new Byte(0)); xs.add(new Byte(1)); Byte x = (Byte)xs.iterator().next(); // string list List ys = new LinkedList(); ys.add("zero"); ys.add("one"); String y = (String)ys.iterator().next(); // string list list List zss = new LinkedList(); zss.add(ys); String z = (String)((List)zss.iterator().next()).iterator().next(); // string list treated as byte list Byte w = (Byte)ys.iterator().next(); // run-time exception } }
測試用的那個 class 建造出一些 lists,然後從中萃取元素。使用者必須記住儲存於 list 之中的元素是什麼型別,然後在萃取元素的同時,將它轉為適當的型別。最後一次萃取行動需要兩個轉型動作。假如使用者出人意表地打算從一個 list of strings 中萃取出一個 byte,便會在執行時期發生異常(exception)。
例二所顯示的是以 GJ 完成的 lists, iterators, linked lists,以及一個測試用的 class?,F在,每個 interfaces 和 class 都有一個型別叁數 A,寫在角括號內,用來表述元素的型別。先前Object 所出現的每一個地方,現在都以 A 取代。先前 List, Iterator, 或 LinkedList 所出現的每一個地方,現在都分別以 List<A>, Iterator<A>, 或 LinkedList<A> 取代。再也不需要仰賴使用者的記憶了,這些叁數便足以說明每一個 list 的元素型別,而且再也不需要轉型動作了。從一個 list of lists 身上萃取元素,現在變得比較明白。假如企圖從一個 list of strings 身上萃取 byte,編譯器會指出錯誤。
例二:Lists, iterators, linked lists, 以及測試用的 class rewritten in GJ interface List<A> { public void add(A x); public Iterator<A> iterator(); } interface Iterator<A> { public A next(); public boolean hasNext(); } class LinkedList<A> implements List<A> { ... } class Test { public static void main (String[] args) { // byte list List<Byte> xs = new LinkedList<Byte>(); xs.add(new Byte(0)); xs.add(new Byte(1)); Byte x = xs.iterator().next(); // string list List<String> ys = new LinkedList<String>(); ys.add("zero"); ys.add("one"); String y = ys.iterator().next(); // string list list List<List<String>> zss = new LinkedList<List<String>>(); zss.add(ys); String z = zss.iterator().next().iterator().next(); // string list treated as byte list Byte w = ys.iterator().next(); // compile-time error } }
例三還展示了一個 Lists class 和一個測試用的 class。前者帶有一個 static method,用來找出 list 中的最大元素。max method 接受一個非空的 list,其內都是可比較的元素,傳回的則是所有元素中最大的一個。Test class 示范了兩個呼叫動作。和先前情況一樣,使用者必須記住他使用的是什麼型別,并做適當轉型。Booleans 并未實作出 comparable interface,所以假如企圖找出一個「以 Booleans 為元素」的 list 中的最大值,執行時期會發生異常(exception)。
例三:以下是 Comparable interface 的宣告,以及實作出該介面的 Byte class 的宣告。 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; } // bridge public int compareTo (Object that) { return this.compareTo((Byte)that); } } class Lists { public static Comparable max (List 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; } } class Test { public static void main (String[] args) { // byte list List xs = new LinkedList(); xs.add(new Byte(0)); xs.add(new Byte(1)); Byte x = (Byte)Lists.max(xs); // boolean list List ys = new LinkedList(); ys.add(new Boolean(false)); ys.add(new Boolean(true)); Boolean y = (Boolean)Lists.max(ys); // run-time exception } }
例四:Code rewritten in GJ interface Comparable<A> { public int compareTo (A that); } class Byte implements Comparable<Byte> { 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; } } class Lists { public static <A implements Comparable<A>> A max (List<A> xs) { Iterator<A> xi = xs.iterator(); A w = xi.next(); while (xi.hasNext()) { A x = xi.next(); if (w.compareTo(x) < 0) w = x; } return w; } } class Test { public static void main (String[] args) { // byte collection LinkedList<Byte> xs = new LinkedList<Byte>(); xs.add(new Byte(0)); xs.add(new Byte(1)); Byte x = Collections.max(xs); // boolean collection LinkedList<Boolean> ys = new LinkedList<Boolean>(); ys.add(new Boolean(false)); ys.add(new Boolean(true)); Boolean y = Collections.max(ys); // compile-time error } } // 譯注:這里改用Collections.max(),而非前例的Lists.max()。 // 兩者實作手法幾乎完全相同,前者可見 srcjavautilcollections.java max method 的標記式(signature)說明了 GJ 的兩個有趣性質 -- generic methods 和 bounds。下面是 max 的標記全貌:
public static <A implements Comparable<A>> A max (List<A> xs)
這個長長的句子告訴我們, max method 接受一個 list,其內的元素型別都是 A,傳回一個元素,型別亦為 A。最前面的角括號內宣告了型別叁數 A,并指出這個 method 可以被任意型別 A 具現化(instantiated),只要 A 實作出 Comparable<A> 介面。一個 method 假如擁有自己的型別叁數,我們稱它為一個 "generic method",而一個型別叁數假如必須實作出某個已知介面(或者必須是某個已知 class 的 subclass),我們稱之為 "bounded"(被限制)。
Test class 示范了兩個實際呼叫動作。在第一個呼叫動作中,編譯器自動推導出 max method 標記式中的型別叁數 A 必須被具現化為 Byte,并且檢查 class Byte 確實實作了 bound Comparable<Byte>。第二次呼叫推導出來的型別叁數是 Boolean,但它并未實作出 bound Comparable<Boolean>。也因此,Java 執行期原本會發出異常(exception)之處,GJ 在編譯期就把它們指出來了。
一般而言,導入一個 bound 的方式是,在型別叁數之後寫上 "implements" 然後再加上一個 interface 名稱,或者是在型別叁數之後寫上 "extends" 然後再加上一個 class 名稱。不論是在 class head 或是 generic method 標記式中,凡型別叁數可以出現之處,bounds 都可以出現。bounding interface 或 class 本身可能又被叁數化,甚至可能形成遞回(recursive),例如上述例子中的 bound Comparable<A> 就內含了 bounded 型別叁數 A。
現在,擦拭(erasure)的定義需要再擴充:一個型別變數經過擦拭,相當於其 bound 的擦拭結果(這麼一來 max method 中的 A 便被擦拭為 Comparable)。一如先前所說,轉譯會導致適當的轉型,也會帶來必要的 bridge methods。和先前所說一樣,例四的 GJ 代碼經過轉譯之後,會導致例三的 Java 代碼(犯錯的那行除外)。
如你所見,GJ 代碼被編譯為 Java 之後,其結果就像你在無泛型性質的情況下所寫的代碼。所以,只要使用GJ編譯器的一個非凡翻新模式(retrofitting mode),你總是可以為舊式的傳統代碼加上型別資訊,甚至即使你手上只有二進位的 class files。
舉個例子,假設你有一個 class file,其內有先前所描述的 Java 版的 LinkedList class,但你希望以 GJ 形式來使用它。例五顯示必要的翻新檔。
例五:翻新檔(retrofitting file) class LinkedList<A> implements Collection<A> { public LinkedList (); public void add (A elt); public Iterator<A> iterator (); }
為了支援獨立編譯,GJ 編譯器將額外的型別標記(type-signature)儲存於 JVM class files 中。class file 檔案格式答應如此的擴充,并於執行時期被 JVM 忽略。翻新器(retrofitter)會取出原有的 Java class file,檢查其型別標記是否如同「GJ 標記被擦拭後」的結果,然後產生一個帶有 GJ 標記的嶄新 class file。
Java2 的整個 collection class library已經以此方式翻新。程式庫中的每一個 public interface, class, 和 method 都有一個適當對應的 GJ 型別,絕無漏網之魚。由於翻新後的 class files 與原先不同之處只在於新增加的那些 GJ 型別標記(它會於執行時期被 JVM 忽略),所以你可以將其結果在一個與 Java 2 相容的瀏覽器中執行,不需重新載入 collection class library。
C++ templates 是以膨脹法(eXPansion)實作出來,編譯器針對被運用的每一個型別,為泛型代碼產生一份對應副本。例如在我們的第一個例子中,會產生三份副本,一個用於 bytes, 一個用於 strings,一個用於 lists of strings。這往往導致程式碼的體積膨脹。猶有進者,由於 template 可能被定義於一個檔案之中而被另一個檔案使用,膨脹法所引起的錯誤往往無法被偵測出來,直到聯結時期才會記錄,而且往往難以回溯。我的同事 Brian Kernighan 和 Rob Pike 曾經寫過一個小型的 C++ 程式,其 templates 產生出一個名稱長達 1594 字元的變數(見 The Practice of Programming, Addison-Wesley, 1999.)