克服 JSR-14 原型编译器中泛型的限制 |
级别: 初级 Eric E. Allen (eallen@cs.rice.edu), 博士研究生, Java 编程语言团队,Rice 大学 2003 年 6 月 09 日 Java 开发人员和研究员 Eric Allen 继续讨论 JSR-14 和 Tiger 中的泛型类型,并着眼于在泛型类型中添加 naked 类型参数的 new 操作支持这一分支。 这一系列主要讨论在 Java 编程中添加泛型类型,本文是其中的一篇,将研究还未讨论过的有关使用泛型的两个限制之一,即添加对裸类型参数的 正如我 上个月所提到的那样,Tiger 和 JSR-14 通过使用“类型消除(type erasure)”对 Java 语言实现泛型类型。使用类型消除(type erasure),泛型类型仅用于类型检查;然后,用它们的上界替换它们。由此定义可知:消除将与如 如果假定 要添加对表达式(如 尤其是,为了实现对 Java 语言添加这种支持,必须对许多基本的语言设计问题作出决定。 首先,为了对类型参数构造合法的
只要求类型参数的所有实例化都包括不带参数的构造函数。该解决方案的优点是非常简单。使用这种方法也有先例。 处理类似问题的现有 Java 技术(象 JavaBean 技术)就是通过要求一个不带参数的构造函数来解决问题的。然而,该方法的一个主要缺点是:对于许多类,没有合理的不带参数的构造函数。 例如,表示非空容器的任何类在构造函数中必然使用表示其元素的参数。包括不带参数的构造函数将迫使我们先创建实例,然后再进行本来可以在构造函数调用中完成的初始化。但该实践会导致问题的产生(您可能想要阅读 2002 年 4 月发表的本专栏文章“The Run-on Initializer bug pattern”,以获取详细信息;请参阅 参考资料。) 处理该问题的另一种方法是:只要泛型类的运行时实例化没有包括所需构造函数,就抛出异常。请注意:必须在运行时抛出异常。因为 Java 语言的递增式编译模型,所以我们无法静态地确定所有将在运行时发生的泛型类的实例化。例如,假设我们有如下一组泛型类: 清单 1.“裸”类型参数的 New 操作
现在,在类 比方说,如果 此外,这一技术所产生的错误种类对于程序员来说很难诊断和修复。例如,假设程序员只熟悉类 还有,对所报告错误的堆栈跟踪甚至可能不包括任何对这个糟糕的 另一种可能性是修改语言语法以包括更详尽的类型参数界限。这些界限可以指定一组可用的构造函数,它们必须出现在参数的每一个实例化中。因而,在泛型类定义内部,唯一可调用的构造函数是那些在界限中声明的构造函数。 同样,实例化泛型类的客户机类必须使用满足对构造函数存在所声明的约束的类来这样做。参数声明将充当类与其客户机之间的契约,这样我们可以静态地检查这两者是否遵守契约。 与另外两种方法相比,该方法有许多优点,它允许我们保持第二种方法的可表达性以及与第一种方法中相同的静态检查程度。但它也有需要克服的问题。 首先,类型参数声明很容易变得冗长。我们或许需要某种形式的语法上的甜头,使这些扩充的参数声明还过得去。另外,如果在 Tiger 以后的版本中添加扩充的参数声明,那么我们必须确保这些扩充的声明将与现有的已编译泛型类兼容。 如果将对泛型类型的与类型相关的操作的支持添加到 Java 编程中,那么它采用何种形式还不清楚。但是,从哪种方法将使 Java 代码尽可能地保持健壮(以及使在它遭到破坏时尽可能容易地修正)的观点看,第三个选项无疑是最适合的。 然而,
更严重的问题是类定义中可能存在 多态递归。当泛型类在其自己的主体中实例化其本身时,发生多态递归。例如,考虑下面的错误示例: 清单 2. 自引用的泛型类
假设客户机类创建新的 为什么这成为问题呢?因为如果我们通过为每个实例化构造独立类来支持泛型类型的与类型相关的操作,那么,在程序运行以前,我们无法知道我们需要构造哪些类。但是,如果类装入器为它所装入的每个类查找现有类文件,那么它会如何工作呢? 同样,这里有几种可能的解决办法:
我们对程序可以产生的泛型类的实例化数目设置上限。然后,在编译期间,我们可以对一组合法的实例化确定有限界限,并且仅为该界限中的所有实例化生成类文件。 该方法类似于在 C++ 标准模板库中完成的事情(这使我们有理由担心它不是一个好方法)。该方法的问题是,和为错误的构造函数调用报告错误一样,程序员将无法预知其程序的某一次运行将崩溃。例如,假设实例化数的界限为 42,并且使用用户提供的参数调用先前提到的 为什么我们不向编译器发出类似“静态禁止多态递归”这样的命令呢?(唉!要是那么简单就好了。)当然,包括我在内的许多程序员都会反对这种策略,它抑制了许多重要设计模式的使用。 例如,在泛型类 下面的示例涉及两个类之间的多态递归: 清单 3. 相互递归的多态递归
在类 或许,我们可以将新属性添加到类文件中,以表明类中所有不同泛型类型实例化,然后在编译其它类时分析这些实例化,以进行递归。但是,我们还是必须向程序员提供奇怪的和不直观的错误消息。 在上面的代码中,我们在哪里报告错误呢?在类 另一种方法是在程序运行时按需构造新的实例化类。起先,这种方法似乎与 Java 运行时完全不兼容。但实际上,实现该策略所需的全部就是使用一个修改的类装入器,它根据“模板(template)”类文件构造新的实例化类。 JVM 规范已经允许程序员使用修改的类装入器;事实上,许多流行的 Java 应用程序(如 Ant、JUnit 和 DrJava)都使用它们。该方法的缺点是:修改的类装入器必须与其应用程序一起分布,以在较旧的 JVM 上运行。因为类装入器往往比较小,所以这个开销不会大。 让我们研究一下该方法的工作示例。 前一种方法 ― 用按需构造泛型类型实例化的修改的类装入器解决多态递归问题 ― 被 Java 语言的 NextGen 扩展所采用。修改的类装入器使用看上去几乎与普通类文件完全一样的模板文件,不同的是这个模板文件在常量池中有一些“洞”,在装入时为每个实例化类填充这些“洞”。非泛型类不受影响。 在 Rice 大学 JavaPLT 编程语言实验室,我们最近发布了 NextGen 编译器的原型,它是 GJ 泛型 Java 编译器的一种扩展,这种扩展支持类型参数的与类型相关的操作(数据类型转换、
正如上述考虑事项所演示的那样,将成熟的运行时支持添加到泛型 Java 要解决许多微妙的设计问题。如果这些问题处理得不当,那么可表达性和健壮性的降低会轻易地抵消泛型类型的好处。但愿 Java 编程会继续朝着维持这些属性的高度表达性和健壮性的方向发展。 下一次,我们将通过讨论或许是功能最强大的应用泛型类型的方法 ― 将 mixin(具有参数父类型的类)添加到语言中 ― 来结束对泛型类型的讨论。我们会将这种 mixin 的表现方式与先前讨论的这种功能强大的语言特性相关联,讨论通过泛型类型添加 mixin 的优缺点。
|