J2SE 1.5 - 代号为 Tiger - 计划在 2003 年年底发布。我一直都热衷于尽可能多地收集有关即将推出的新技术的预告信息,因此我将撰写一系列的文章,讨论可从 V1.5 中获得的新的和经过重组的特性,本文是第一篇。
J2SE 1.5 - 代号为 Tiger - 计划在 2003 年年底发布。我一直都热衷于尽可能多地收集有关即将推出的新技术的预告信息,因此我将撰写一系列的文章,讨论可从 V1.5 中获得的新的和经过重组的特性,本文是第一篇。我特别想谈谈泛型类型并重点讲述在 Tiger 中为了支持它们而进行的更改和调整。
在许多方面,Tiger 肯定是迄今为止在 Java 编程方面(包括对源语言语法的重大扩展)所取得的最大进步。Tiger 中计划进行的最显著的变化是添加泛型类型,正如在 JSR-14 原型编译器中所预先展示的那样(您可以立即免费下载该编译器;请参阅参考资料)。
让我们从介绍泛型类型是什么以及添加了什么特性来支持它们开始吧。
数据类型转换和错误 为理解泛型类型为何如此有用,我们要将注意力转向 Java 语言中最容易引发错误的因素之一 - 需要不断地将表达式向下类型转换(downcast)为比其静态类型更为具体的数据类型(请参阅参考资料中的“The Double Descent bug pattern”,以了解进行数据类型转换时,可能会碰到的麻烦的某些方面)。
程序中的每个向下类型转换对于 ClassCastException 而言都是潜在的危险,应当尽量避免它们。但是在 Java 语言中它们通常是无法避免的,即便在设计优良的程序中也是如此。
在 Java 语言中进行向下类型转换最常见的原因在于,经常以专用的方式来使用类,这限制了方法调用所返回的参数可能的运行时类型。例如,假定往 Hashtable 中添加元素并从中检索元素。那么在给定的程序中,被用作键的元素类型和存储在散列表中的值类型,将不能是任意对象。通常,所有的键都是某一特定类型的实例。同样地,存储的值将共同具有比 Object 更具体的公共类型。
但是在目前现有的 Java 语言版本中,不可能将散列表的特定键和元素声明为比 Object 更具体的类型。在散列表上执行插入和检索操作的类型特征符告诉我们只能插入和删除任意对象。例如,put 和 get 操作的说明如下所示:
清单 1. 插入/检索类型说明表明只能是任意对象
class Hashtable {
Object put(Object key, Object value) {...}
Object get(Object key) {...}
...
}
因此,当我们从类 Hashtable 的实例检索元素时,比如,即使我们知道在 Hashtable 中只放了 String,而类型系统也只知道所检索的值是 Object 类型。在对检索到的值进行任何特定于 String 的操作之前,必须将它强制转换为 String,即使是将检索到的元素添加到同一代码块中,也是如此!
清单 2. 将检索到的值强制转换成 String
import java.util.Hashtable;
class Test {
public static void main(String[] args) {
Hashtable h = new Hashtable();
h.put(new Integer(0), "value");
String s = (String)h.get(new Integer(0));
System.out.println(s);
}
}
请注意 main 方法主体部分的第三行中需要进行的数据类型转换。因为 Java 类型系统相当薄弱,因此代码会因象上面那样的数据类型转换而漏洞百出。这些数据类型转换不仅使 Java 代码变得更加拖沓冗长,而且它们还降低了静态类型检查的价值(因为每个数据类型转换都是一个选择忽略静态类型检查的伪指令)。我们该如何扩展该类型系统,从而不必回避它呢?
用泛型类型来解决问题! 要消除如上所述的数据类型转换,有一种普遍的方法,就是用泛型类型来增大 Java 类型系统。可以将泛型类型看作是类型“函数”;它们通过类型变量进行参数化,这些类型变量可以根据上下文用各种类型参数进行实例化。
例如,与简单地定义类 Hashtable 不同,我们可以定义泛型类 Hashtable< Key, Value>,其中 Key 和 Value 是类型参数。除了类名后跟着尖括号括起来的一系列类型参数声明之外,在 Tiger 中定义这样的泛型类的语法和用于定义普通类的语法很相似。例如,可以按照如下所示的那样定义自己的泛型 Hashtable 类:
清单 3. 定义泛型 Hashtable 类
class Hashtable< Key, Value> { ... }
然后可以引用这些类型参数,就像我们在类定义主体内引用普通类型那样,如下所示:
清单 4. 像引用普通类型那样引用类型参数 class Hashtable< Key, Value> {
...
Value put(Key k, Value v) {...}
Value get(Key k) {...}
}
类型参数的作用域就是相应类定义的主体部分(除了静态成员之外)(在下一篇文章中,我们将讨论为何 Tiger 实现中有这样的“怪习”,即必须对静态成员进行此项限制。请留意!)。
创建一个新的 Hashtable 实例时,必须传递类型参数以指定 Key 和 Value 的类型。传递类型参数的方式取决于我们打算如何使用 Hashtable。在上面的示例中,我们真正想要做的是创建 Hashtable 实例,它只将 Integer 映射为 String。可以用新的 Hashtable 类来完成这件事:
清单 5. 创建将 Integer 映射为 String 的实例
import java.util.Hashtable;
class Test {
public static void main(String[] args) {
Hashtable< Integer, String> h = new Hashtable< Integer, String>();
h.put(new Integer(0), "value");
...
}
}
现在不再需要数据类型转换了。请注意用来实例化泛型类 Hashtable 的语法。就像泛型类的类型参数用尖括号括起来那样,泛型类型应用程序的参数也是用尖括号括起来的。
清单 6. 除去不必要的数据类型转换
...
String s = h.get("key");
System.out.println(s);
当然,程序员若只是为了能使用泛型类型而必须重新定义所有的标准实用程序类(比如 Hashtable 和 List)的话,则可能会是一项浩大的工程。幸好,Tiger 为用户提供了所有 Java 集合类的泛型版本,因此我们不必自己动手来重新定义它们了。此外,这些类能与旧代码和新的泛型代码一起无缝工作(下个月,我们会说明如何做到这一点)。
Tiger 的基本类型限制 Tiger 中类型变量的限制之一就是,它们必须用引用类型进行实例化 - 基本类型不起作用。因此,在上面这个示例中,无法完成创建从 int 映射到 String 的 Hashtable。
这很遗憾,因为这意味着只要您想把基本类型用作泛型类型的参数,您就必须把它们组装为对象。另一方面,当前的这种情况是最糟的;您不能将 int 作为键传递给 Hashtable,因为所有的键都必须是 Object 类型。
我们真正想看到的是,基本类型可以自动进行包装(boxing)和解包装(unboxing),类似于用 C# 所进行的操作(或者比后者更好)。遗憾的是,Tiger 不打算包括基本类型的自动包装(但是人们可以一直期待 Java 1.6 中出现该功能!)。
受限泛型 有时我们想限制可能出现的泛型类的类型实例化。在上面这个示例中,类 Hashtable 的类型参数可以用我们想用的任何类型参数进行实例化,但是对于其它某些类,我们或许想将可能的类型参数集限定为给定类型范围内的子类型。
例如,我们可能想定义泛型 ScrollPane 类,它引用普通的带有滚动条功能的 Pane。被包含的 Pane 的运行时类型通常会是类 Pane 的子类型,但是静态类型就只是 Pane。
有时我们想用 getter 检索被包含的 Pane,但是希望 getter 的返回类型尽可能具体些。我们可能想将类型参数 MyPane 添加到 ScrollPane 中,该类型参数可以用 Pane 的任何子类进行实例化。然后可以用这种形式的子句:extends Bound 来说明 MyPane 的声明,从而来设定 MyPane 的范围:
清单 7. 用 extends 子句来说明 MyPane 声明
class ScrollPane< MyPane extends Pane> { ... }
当然,我们可以完全不使用显式的范围,只要能确保没有用不适当的类型来实例化类型参数。
为什么要自找麻烦在类型参数上设定范围呢?这里有两个原因。首先,范围使我们增加了静态类型检查功能。有了静态类型检查,就能保证泛型类型的每次实例化都符合所设定的范围。
其次,因为我们知道类型参数的每次实例化都是这个范围之内的子类,所以可以放心地调用类型参数实例出现在这个范围之内的任何方法。如果没有对参数设定显式的范围,那么缺省情况下范围是 Object,这意味着我们不能调用范围实例在 Object 中未曾出现的任何方法。
多态方法 除了用类型参数对类进行参数化之外,用类型参数对方法进行参数化往往也同样很有用。泛型 Java 编程用语中,用类型进行参数化的方法被称为多态方法(Polymorphic method)。
多态方法之所以有用,是因为有时候,在一些我们想执行的操作中,参数与返回值之间的类型相关性原本就是泛型的,但是这个泛型性质不依赖于任何类级的类型信息,而且对于各个方法调用都不相同。
例如,假定想将 factory 方法添加到 List 类中。这个静态方法只带一个参数,也将是 List 唯一的元素(直到添加了其它元素)。因为我们希望 List 成为其所包含的元素类型的泛型,所以希望静态 factory 方法带有类型变量 T 这一参数并返回 List< T> 的实例。
但是我们确实希望该类型变量 T 能在方法级别上进行声明,因为它会随每次单独的方法调用而发生改变(而且,正如我在下一篇文章中将讨论的那样,Tiger 设计的“怪习”规定静态成员不在类级类型参数的范畴之内)
查看本文来源
J2SE 1.5 ― 代号为“Tiger” ― 计划在 2003 年年底发布,它将包括泛型类型(如在 JSR-14 原型编译器中预先展示的那样,现在可下载获得)。在第 1 部分中,我们讨论了泛型类型的基础知识,以及为什么它们是对 Java 语言的一个重要且迫切需要的补充。我们还说明了为 Tiger 制定的泛型类型的实现怎么会包含数个“缺陷”,这些缺陷限制了可以使用泛型类型的上下文。
为了帮助新程序员有效地使用泛型类型,我将详细说明到底泛型类型的哪些用法在 Tiger 和 JSR-14 中是被禁止的,并将说明为什么这些限制是 JSR-14(理所当然还有 Tiger)为了在 JVM 上兼容地实现泛型类型所使用的实现策略的必然结果。
泛型类型的限制 让我们先查阅一下 Tiger 和 JSR-14 中泛型类型的使用限制:
不应在静态成员中引用封闭类型参数。
不能用基本类型实例化泛型类型参数。
不能在数据类型转换或 instanceof 操作中使用“外露”类型参数。
不能在 new 操作中使用“外露”类型参数。
不能在类定义的 implements 或 extends 子句中使用“外露”类型参数。
为什么会有这些限制呢?这要归因于 Tiger 和 JSR-14 为在 JVM 上实现泛型类型所使用的机制。由于 JVM 根本不支持泛型类型,所以这些编译器“耍了个花招”,使得似乎存在对泛型类型的支持 ― 它们用泛型类型信息检查所有的代码,但随即“擦除”所有的泛型类型并生成只包含普通类型的类文件。
例如,将象 List< T> 这样的泛型类型擦除得只剩下 List。“外露”类型参数 ― 单独出现而不是位于某个类型中的类型参数(如类 List< T> 中的类型参数 T)― 被简单地擦除成它们的上界(就 T 而言,其上界就是 Object)。
这一技术的功能极其强大;我们可以使几乎所有泛型类型的精度得到增强,但又与 JVM 保持兼容。事实上,我们甚至可以交替地使用非泛型的旧类(比如 List)和其对应的泛型类(List< T>);两者在运行时看起来是一样的。
遗憾的是,正如以上的限制所示,获得这一功能是有代价的。以这种方式进行擦除在类型系统中引入了缺陷,这些缺陷限制我们使用泛型类型的安全性。
为了帮助说明每种限制,我们查阅会出现这些限制的示例。在本文中,我们将讨论前三个限制。与后两个限制有关的问题过于复杂,因而需要更深入的研究,留待下一篇文章讨论。
静态成员中的封闭类型参数 编译器完全禁止在静态方法和静态内部类中引用封闭类型参数。所以,举例来说,以下代码在 Tiger 中就是非法的:
清单 1. 在静态上下文中非法引用封闭类型参数 class C< T> {
static void m() {
T t;
}
static class D {
C< T> t;
}
}
当编译这一代码时,会生成两个错误:
在静态方法 m 中非法引用 T 的错误
在静态类 D 中非法引用 T 的错误
当定义静态字段时,情况变得更加复杂。在 JSR-14 和 Tiger 中,在泛型类的所有实例中共享该类中的静态字段。现在,在 JSR-14 编译器 1.0 和 1.2 中,如果您在静态字段声明中引用类型参数,编译器不会报错,但它本应该这么做。字段被共享这一事实很容易在运行时导致奇怪的错误,如在不包含数据类型转换的代码中出现 ClassCastException。
例如,以下程序将在这两个版本的 JSR-14 下通过编译而没有任何警告:
清单 2. 在静态字段中对封闭类型参数的有问题的引用 class C< T> {
static T member;
C(T t) { member = t; }
T getMember() { return member; }
public static void main(String[] args) {
C< String> c = new C< String>("test");
System.out.println(c.getMember().toString());
new C< Integer>(new Integer(1));
System.out.println(c.getMember().toString());
}
}
请注意,每次分配类 C 的实例时,都要重新设置静态字段 member。而且,它被设置成的对象类型取决于 C 的实例的类型!在所提供的 main 方法中,第一个实例 c 是 C< String> 类型。而第二个是 C< Integer> 类型。每当从 c 访问 member 这一共享静态字段时,总是假定 member 的类型是 String。但是,在分配了类型为 C< Integer> 的第二个实例之后,member 的类型是 Integer。
运行 C 的 main 方法的结果可能会让您吃惊 ― 它将发出一个 ClassCastException!源代码根本没有包含任何数据类型转换,怎么会这样呢?事实证明编译器确实在编译阶段将数据类型转换插入到代码中,这样做是为了解决类型擦除会降低某些表达式的类型的精度这一事实。这些数据类型转换被期望能够成功,但在本例中却没有成功。
应该认为 JSR-14 1.0 和 1.2 的这一特殊“功能”是个错误。它破坏了类型系统的健全性,或者可以说,它破坏了类型系统应该和程序员达成的“基本契约”。象对静态方法和类所做的那样,只要防止程序员在静态字段中引用泛型类型,情况就会好很多。
请注意允许这种有潜在“爆炸性”的代码存在所带来的问题并不是程序员有意在自己的代码中覆盖类型系统。问题是程序员可能会无意中编写这样的代码(比如,由于“复制和粘贴”操作,错误地在字段声明中包括静态修饰符)。
类型检查器应该能帮助程序员从这些类型的错误中恢复,但对于静态字段而言,类型系统实际上会使程序员更迷惑。当未使用数据类型转换的代码中显示的唯一错误就是 ClassCastException 时,我们应如何诊断这样的错误?对于不清楚 Tiger 中泛型类型所用的实现方案而又恰好假定类型系统合理运行的程序员而言,情况更糟。因为在这样的情况下,类型系统不是合理地运行。
幸运的是,JSR-14 的最新版本(1.3)宣布在静态字段中使用类型参数是不合法的。因此,我们有理由期待在 Tiger 的静态字段中使用类型参数也是不合法的。
泛型类型参数和基本类型 和我们刚才讨论的不同,这一限制没有同样的潜在缺陷,但它会使您的代码非常冗长。例如,在 java.util.Hashtable 的泛型版本中,有两种类型参数:用于 Key 类型的和用于 Value 类型的。因此,如果我们想要一个将 String 映射到 String 的 Hashtable,我们可以用表达式 new Hashtable< String, String>() 指定新的实例。但是,如果我们想要一个将 String 映射到 int 的 Hashtable,我们只能创建 Hashtable< String, Integer> 的实例,并将所有的 int 值包装在 Integer 中。
同样,Tiger 在这方面当然也是由所用的实现方案得到的。既然类型参数被擦除为它们的界限,而界限不能是基本类型,所以一旦类型被擦除,则对基本类型的实例化会完全没有意义。
数据类型转换或 instanceof 操作中的“外露”参数 回想一下,对于“外露”类型参数,我们是指在词汇上单独出现的类型参数,而不是更大类型的语法子组件。例如,C< T> 不是“外露”类型参数,但(在 C 主体中)T 是。
如果在代码中对“外露”类型参数进行数据类型转换或 instanceof 操作,则编译器将发出名为“unchecked”的警告。例如,以下代码将生成警告:Warning: unchecked cast to type T:
清单 3. 带 unchecked 操作的泛型代码 import java.util.Hashtable;
interface Registry {
public void register(Object o);
}
class C< T> implements Registry {
int counter = 0;
Hashtable< Integer, T> values;
public C() {
values = new Hashtable< Integer, T>();
}
public void register(Object o) {
values.put(new Integer(counter), (T)o);
counter++;
}
}
您应该严肃地对待这些警告,因为它们说明您的代码在运行时会表现得非常奇怪。事实上,它们会使得诊断代码变得极为困难。在以前的代码中,我们认为如果对实例 C< JFrame> 调用 register("test"),会发出 ClassCastException。但并非如此;计算将继续,就仿佛数据类型转换成功了一样,然后在进一步进行计算时发出错误,或者更糟:用遭破坏的数据完成计算,但不向外发出任何错误信号。同样,对“外露”类型参数的 instanceof 检查将在编译时产生“unchecked”警告,而且检查将不会如期在运行时进行。
双刃剑 那么,这里到底发生了什么?因为 Tiger 依靠类型擦除,所以数据类型转换和 instanceof 测试中的外露类型参数被“擦除”为它们的上界(在前面的例子中,那将是类型 Object)。因此,对类型参数的数据类型转换将变成对参数上界的转换。
同样,instanceof 将检查操作数是否是参数界限的 instanceof。那根本不是我们打算做的,如果是的话,我们完全可以显式地强制转换为界限。因此,通常应避免对类型参数使用数据类型转换和 instanceof 检查。
然而,有时为了编译代码,您必须依靠对类型参数的数据类型转换。如果是这样的情况,只要记住,在代码的那一部分中,类型检查不保险 ― 要靠自己。
尽管泛型类型是制作健壮代码的强大武器,但我们已经演示了误用它们会使代码不再健壮而且极难诊断和修正。下次,我们将介绍 Tiger 中泛型类型的后两个限制,并讨论试图在泛型 Java 类型系统中包括它们时必定会出现的一些问题。
查看本文来源
有效的构造函数调用 首先,为了对类型参数构造合法的 new 表达式(如 new T()),必须确保我们调用的构造函数对于 T 的每个实例化都有效。但由于我们只知道 T 是其已声明界限的子类型,所以我们不知道 T 的某一实例化将有什么构造函数。要解决这一问题,可以用下述三种方法之一:
要求类型参数的所有实例化都包括不带参数的(zeroary)构造函数。
只要泛型类的运行时实例化没有包括所需的构造函数,就抛出异常。
修改语言的语法以包括更详尽的类型参数界限。
第 1 种方法:需要不带参数的构造函数 只要求类型参数的所有实例化都包括不带参数的构造函数。该解决方案的优点是非常简单。使用这种方法也有先例。
处理类似问题的现有 Java 技术(象 JavaBean 技术)就是通过要求一个不带参数的构造函数来解决问题的。然而,该方法的一个主要缺点是:对于许多类,没有合理的不带参数的构造函数。
例如,表示非空容器的任何类在构造函数中必然使用表示其元素的参数。包括不带参数的构造函数将迫使我们先创建实例,然后再进行本来可以在构造函数调用中完成的初始化。但该实践会导致问题的产生(您可能想要阅读 2002 年 4 月发表的本专栏文章“The Run-on Initializer bug pattern”,以获取详细信息;请参阅参考资料。)
第 2 种方法:当缺少所需构造函数时,抛出异常 处理该问题的另一种方法是:只要泛型类的运行时实例化没有包括所需构造函数,就抛出异常。请注意:必须在运行时抛出异常。因为 Java 语言的递增式编译模型,所以我们无法静态地确定所有将在运行时发生的泛型类的实例化。例如,假设我们有如下一组泛型类:
清单 1.“裸”类型参数的 New 操作
class C< T> {
T makeT() {
return new T();
}
}
class D< S> {
C< S> makeC() {
return new C< S>();
}
}
现在,在类 D< S> 中,构造了类 C< S> 的实例。然后,在类 C 的主体中,将调用 S 的不带参数的构造函数。这种不带参数的构造函数存在吗?答案当然取决于 S 的实例化!
比方说,如果 S 被实例化为 String,那么答案是“存在”。如果它被实例化为 Integer,那么答案是“不存在”。但是,当编译类 D 和 C 时,我们不知道其它类会构造什么样的 D< S> 实例化。即使我们有可用于分析的整个程序(我们几乎从来没有这样的 Java 程序),我们还是必须进行代价相当高的流分析来确定潜在的构造函数问题可能会出现在哪里。
此外,这一技术所产生的错误种类对于程序员来说很难诊断和修复。例如,假设程序员只熟悉类 D 的头。他知道 D 的类型参数的界限是缺省界限(Object)。如果得到那样的信息,他没有理由相信满足声明类型界限(如 D< Integer>)的 D 的实例化将会导致错误。事实上,它在相当长的时间里都不会引起错误,直到最后有人调用方法 makeC 以及(最终)对 C 的实例化调用方法 makeT。然后,我们将得到一个报告的错误,但这将在实际问题发生很久以后 ― 类 D 的糟糕实例化。
还有,对所报告错误的堆栈跟踪甚至可能不包括任何对这个糟糕的 D 实例的方法调用!现在,让我们假设程序员无权访问类 C 的源代码。他对问题是什么或如何修正代码将毫无头绪,除非他设法联系类 C 的维护者并获得线索。
第 3 种方法:修改语法以获得更详尽的界限 另一种可能性是修改语言语法以包括更详尽的类型参数界限。这些界限可以指定一组可用的构造函数,它们必须出现在参数的每一个实例化中。因而,在泛型类定义内部,唯一可调用的构造函数是那些在界限中声明的构造函数。
同样,实例化泛型类的客户机类必须使用满足对构造函数存在所声明的约束的类来这样做。参数声明将充当类与其客户机之间的契约,这样我们可以静态地检查这两者是否遵守契约。
与另外两种方法相比,该方法有许多优点,它允许我们保持第二种方法的可表达性以及与第一种方法中相同的静态检查程度。但它也有需要克服的问题。
首先,类型参数声明很容易变得冗长。我们或许需要某种形式的语法上的甜头,使这些扩充的参数声明还过得去。另外,如果在 Tiger 以后的版本中添加扩充的参数声明,那么我们必须确保这些扩充的声明将与现有的已编译泛型类兼容。
如果将对泛型类型的与类型相关的操作的支持添加到 Java 编程中,那么它采用何种形式还不清楚。但是,从哪种方法将使 Java 代码尽可能地保持健壮(以及使在它遭到破坏时尽可能容易地修正)的观点看,第三个选项无疑是最适合的。
然而,new 表达式有另一个更严重的问题。
多态递归 更严重的问题是类定义中可能存在多态递归。当泛型类在其自己的主体中实例化其本身时,发生多态递归。例如,考虑下面的错误示例:
清单 2. 自引用的泛型类
class C< T> {
public Object nest(int n) {
if (n == 0) return this;
else return new C< C< T>>().nest(n - 1);
}
}
假设客户机类创建新的 C< Object> 实例,并调用(比方说)nest(1000)。然后,在执行方法 nest() 的过程中,将构造新的实例化 C< C< Object>>,并且对它调用 nest(999)。然后,将构造实例化 C< C< C< Object>>>,以此类推,直到构造 1000 个独立的类 C 的实例化。当然,我随便选择数字 1000;通常,我们无法知道在运行时哪些整数将被传递到方法 nest。事实上,可以将它们作为用户输入传入。
为什么这成为问题呢?因为如果我们通过为每个实例化构造独立类来支持泛型类型的与类型相关的操作,那么,在程序运行以前,我们无法知道我们需要构造哪些类。但是,如果类装入器为它所装入的每个类查找现有类文件,那么它会如何工作呢?
同样,这里有几种可能的解决办法:
对程序可以产生的泛型类的实例化数目设置上限。
静态禁止多态递归。
在程序运行时随需构造新的实例化类。
第 1 种:对实例化数设置上限 我们对程序可以产生的泛型类的实例化数目设置上限。然后,在编译期间,我们可以对一组合法的实例化确定有限界限,并且仅为该界限中的所有实例化生成类文件。
该方法类似于在 C++ 标准模板库中完成的事情(这使我们有理由担心它不是一个好方法)。该方法的问题是,和为错误的构造函数调用报告错误一样,程序员将无法预知其程序的某一次运行将崩溃。例如,假设实例化数的界限为 42,并且使用用户提供的参数调用先前提到的 nest() 方法。那么,只要用户输入小于 42 的数,一切都正常。当用户输入 43 时,这一计划不周的设计就会失败。现在,设想一下可怜的代码维护者,他所面对的任务是重新组合代码并试图弄清楚幻数 42 有什么特殊之处。
第 2 种:静态禁止多态递归 为什么我们不向编译器发出类似“静态禁止多态递归”这样的命令呢?(唉!要是那么简单就好了。)当然,包括我在内的许多程序员都会反对这种策略,它抑制了许多重要设计模式的使用。
例如,在泛型类 List< T> 中,您真的想要防止 List< List< T>> 的构造吗?从方法返回这种列表对于构建许多很常用的数据结构很有用。事实证明我们无法防止多态递归,即使我们想要那样,也是如此。就象静态检测糟糕的泛型构造函数调用一样,禁止多态递归会与递增式类编译发生冲突。我们先前的简单示例(其中,多态递归作为一个简单直接的自引用发生)会使这一事实变得模糊。但是,自引用对于在不同时间编译的大多数类常常采用任意的间接级别。再提一次,那是因为一个泛型类可以用其自己的类型参数来实例化另一个泛型类。
下面的示例涉及两个类之间的多态递归:
清单 3. 相互递归的多态递归
class C< T> {
public Object potentialNest(int n) {
if (n == 0) return this;
else return new D< T>().nest(n - 1);
}
}
class D< S> {
public Object nest(int n) {
return new C< C< S>>().nest(n);
}
}
在类 C 或 D 中显然没有多态递归,但象 new D< C< Object>>().nest(1000) 之类的表达式将引起类 C 的 1000 次实例化。
或许,我们可以将新属性添加到类文件中,以表明类中所有不同泛型类型实例化,然后在编译其它类时分析这些实例化,以进行递归。但是,我们还是必须向程序员提供奇怪的和不直观的错误消息。
在上面的代码中,我们在哪里报告错误呢?在类 D 的编译过程中还是在包含不相干表达式 new D< C< Object>>().nest(1000) 的客户机类的编译过程中呢?无论是哪一种,除非程序员有权访问类 C 的源代码,否则他无法预知何时会发生编译错误。
第 3 种:实时构造新的实例化类 另一种方法是在程序运行时按需构造新的实例化类。起先,这种方法似乎与 Java 运行时完全不兼容。但实际上,实现该策略所需的全部就是使用一个修改的类装入器,它根据“模板(template)”类文件构造新的实例化类。
JVM 规范已经允许程序员使用修改的类装入器;事实上,许多流行的 Java 应用程序(如 Ant、JUnit 和 DrJava)都使用它们。该方法的缺点是:修改的类装入器必须与其应用程序一起分布,以在较旧的 JVM 上运行。因为类装入器往往比较小,所以这个开销不会大。
查看本文来源
Java 开发人员和研究人员 Eric Allen 讨论了通过泛型类型添加对 mixin 支持所带来的影响,并以此文来结束他对 JSR-14 和 Tiger 中泛型类型的由四部分组成的系列的讨论。在相关论坛中与作者及其他读者分享您对本文的看法。(您也可以单击本文顶部或底部的“讨论”来访问该论坛。)至此,在这个讨论 JSR-14 和 Tiger 中泛型类型的微型系列中,我们已经探讨了:
泛型类型及被设计成支持它们的即将发布的功能
基本类型、受约束的泛型以及多态方法上的限制
几个强加给这些 Java 扩展的限制
这些扩展语言的编译器所用的实现策略如何使这些限制成为必需
在泛型类型中添加对“裸”类型参数的 new 操作的支持所带来的影响
本月,我们将探讨在可以处理 mixin(可能被期望是泛型类型中最强大的功能)之前需要先解决的问题,以此来结束对 Java 语言中泛型类型的讨论。
mixin vs 包装
mixin 是由其父类参数化的类。例如,请考虑以下这个泛型类,它继承了它本身的类型参数:
class Scrollable< T> extends T {...}
不要错过本系列的其它文章
第 1 部分,轻松掌握 Java 泛型(2003 年 2 月)
第 2 部分,轻松掌握 Java 泛型类型,第 2 部分(2003 年 3 月)
第 3 部分,轻松掌握 Java 泛型,第 3 部分(2003 年 4 月)
类 Scrollable 的目的是要向 GUI 窗口小部件嵌入添加可滚动性所必需的功能性。这个泛型类的每个应用都会继承一个不同的父类。例如,Scrollable< JTextPane> 是 JTextPane 的子类,而 Scrollable< JEditorPane> 是 JEditorPane 的子类。对比这种嵌入功能的方法和 Java Swing 库中现有的功能性,在这个库中,如果我们想使 JComponent 是可滚动的,必须将其“包装”在 JScrollPane 中。
包装不仅需要添加访问被包装类的功能的转发方法,而且它还阻止我们在需要被包装对象的实例的上下文中使用由此产生的可滚动对象(例如,我们不能将 JScrollPane 传递到需要 JTextPane 的实例的方法中)。通过 Scrollable 的父类将其参数化,在继承多个超类时,我们就能保持对涉及滚动的功能的单点控制。这样能够使用 mixin 让我们重新获得多重继承性的某些强大功能,而又没有附带异常。
在上面的示例中,我们甚至可以对类型参数施加约束以阻止它用于不适当的上下文中。例如,我们可能想使该类型参数强制为 JComponent 的子类:
class Scrollable< T extends JComponent> extends T {...}
那么我们的 mixin 只能继承 GUI 组件。
mixin 和泛型类:完美组合
通常,mixin 作为独立语言功能部件添加到某种语言中,就象 Jam 中的那样。但是合并 mixin 以作为泛型类型系统的一部分很吸引人,几乎可以说魅力无穷。原因是:mixin 和泛型类都能被认为是将现有类映射到新类的函数。
泛型类可被视为将它们的参数映射成新实例化的函数。mixin 可被视为将现有类映射成新子类的函数。通过使用泛型类型合并 mixin,我们能解决其它 mixin 公式的许多关键限制。
在 Java 语言的 Jam 扩展中,mixin 的超类类型没有名称;我们就不能在 mixin 主体中引用它。这一限制会迅速引起一连串各种其它问题。例如,在 Jam 中,禁止程序员将 this 作为参数传递给方法;无法对这样的调用进行类型检查。这一限制的影响极大,因为许多最常见的设计模式都要依赖于能够将 this 作为参数传递。
请考虑访问者模式,其中用 for 方法为复合层次结构中的每个类都定义了访问者类。通常被访问的类包含 accept 方法,它采用访问者并传递 this 来调用该访问者的方法。因此,在 Jam 中,访问者模式不能和 mixin 一起使用。
将 mixin 明确表述为泛型类,我们就始终有父类的句柄,它是该类继承的类型参数。例如,我们可以将 Scrollable 的父类引用为类型 T。其结果是,在允许将 this 作为类型参数传递时没有任何根本性的困难。
但是,将 mixin 明确表述为泛型类型时有其它一些明显的困难。为了让您初步体会可能产生的某些困难,我们将讨论几个突出的困难以及一些可能的解决方案。
mixin 与类型消除
在讨论任何其它问题之前,我们应该先指出,与上月讨论的泛型类型的功能扩展一样,通过使用由 JSR-14 和 Tiger 使用的简单类型消除(type erasure)策略,不能将对 mixin 的支持添加到 Java 语言中。
要了解其原因,请考虑在继承类型参数的类被消除时会出现什么情况。该类会最终继承类型参数的界限!例如,上一个示例中类 Scrollable 的每个实例化最终都继承类 JComponent。那显然不是我们所希望的。
为了通过泛型类型支持 mixin,我们需要获得泛型类型实例化的运行时表示。幸运的是,编码这一信息的方法有许多,它们实际上都向后与 Tiger 兼容。这样的向后兼容编码方案是泛型 Java(Generic Java)的 NextGen 公式的显著特点(在参考资料一节中)。
可用的超类构造函数
在我们希望允许类继承类型参数时立即出现的紧迫问题是要决定我们能调用什么样的超级构造函数?请回忆:每个 Java 类构造函数都必须调用超类的构造函数。通常,通过查找超类并确保存在匹配的超级构造函数,类型检查器确保这些超级构造函数调用会成功。
但是在我们对超类所知的一切只限于它是类型参数的实例化时,对于什么样的构造函数可用于给定的实例化,我们没有任何概念。而且请注意,类型检查器甚至不能检查是否每个 mixin 实例化都会产生有效的超级构造函数调用。其原因是:在某些其它上下文中,mixin 的参数可能用类型参数界限实例化了。
例如,泛型类 JSplitPane< T> 可以创建 Scrollable< T> 的实例。除非我们知道将类型参数 T 实例化为 JSplitPanes 的一切方法,否则我们不能知道在 Scrollable< T> 中调用的超级构造函数是否有效。但是因为 Java 编码允许单独的类编译,所以在类型检查期间,我们不能知道 JSplitPane 的所有实例。
解决这一问题的各种方案与我们上月第 3 部分中讨论的针对检查 new 表达式的类型参数所提出的解决方案完全一致,因为超级构造函数调用和 new 表达式都引用了给定类的同一个类构造函数。让我们回顾一下这些解决方案:
需要一个不带参数的(zeroary)构造函数,用于所有类型参数的实例化。
当没有匹配的构造函数时,抛出运行时异常。
包含额外的类型参数注释,告知我们这些实例化必须包含哪些构造函数。
就如 new 表达式的情况,前两个解决方案有严重缺陷。通常在类定义中包含不带参数的构造函数没有任何意义。而且,当不存在任何匹配的构造函数时就抛出异常也不太理想。毕竟静态类型检查主要是严格防止那种异常。
第三种解决方案可能有点繁琐,但是它有许多优点。注释类型参数,其中包括所有实例化都必须拥有的构造函数集。这些注释确切地告知我们针对类型参数,我们可以可靠地调用什么样的构造函数。因此,当类型参数 T 用作泛型类的超类时,T 的注释确切地告知我们可以调用哪些超级构造函数。如果 T 不包含注释,那么类型检查器会禁止它用作超类。
意外的方法覆盖
任何 mixin 公式都会产生一个非常严重的问题:特定 mixin 的方法名可能与其超类的潜在实例化的方法名冲突。例如,假设类 Scrollable 包含不带任何参数的方法 getSize 并返回一个 Size 对象,编码了其水平和垂直尺寸。现在,我们假设类 MyTextPane(JComponent 的子类)也包含不带任何参数的方法 getSize,但返回一个 int,表示调用它的对象的屏幕面积。
产生的类显示如下:
清单 1. 意外方法覆盖的示例
class Scrollable< T extends JComponent> extends T {
...
Size getSize() {...}
}
class MyTextPane extends JComponent {
...
int getSize() {...}
}
new Scrollable< MyTextPane>()
随后 mixin 实例化 Scrollable< MyTextPane> 会包含两个带有同样(空)参数类型的方法 getSize,但返回类型不一致!因为我们不能指望类 Scrollable 的程序员或 MyTextPane 的程序员预见这个有问题的 getSize 覆盖(毕竟,他们甚至不可能在同一个开发团队),因此我们称之为意外覆盖。
当 mixin 被明确表述为泛型类时,意外覆盖的问题特别讨厌。因为 mixin 的父类可能用类型参数被实例化,因此类型检查器就不能确定意外方法覆盖的所有情况。而且,在意外覆盖出现时抛出运行时异常是无法接受的,因为客户机程序员无法预测何时将抛出这样的异常。如果我们想编写可靠的程序,那么我们必须禁止在运行时出现无法预料的错误。
另一个解决方案是只隐藏这些相互冲突的方法中的一个,并解析所有匹配的方法调用以引用未隐藏的方法。这个解决方案的问题是我们希望诸如 Scrollable< MyTextPane> 这样的 mixin 实例化可用于调用 Scrollable 对象的上下文以及调用 MyTextPane 对象的上下文中。隐藏 getSize 方法中的任一个都会在这两个上下文中禁止使用 Scrollable< MyTextPane>。
在 1998 年召开的有关编程语言原理的 ACM SIGPLAN-SIGACT 研讨会(请参阅参考资料)上,Felleisen、Flatt 和 Krishnamurthi 提出了在 mixin 不属于泛型类型的上下文中针对该问题的一个好的解决方案:基于使用 mixin 实例化的上下文来解决对相互冲突的方法的引用。在这个解决方案中,mixin 包含有
查看本文来源