下面就是「泛型」能够解决的问题。假设你希望运用 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。现在,每个 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.)