内容


诊断 Java 代码

轻松掌握 Java 泛型,第 4 部分

泛型类型如何能管教惹是生非的 mixin

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 诊断 Java 代码

敬请期待该系列的后续内容。

此内容是该系列的一部分:诊断 Java 代码

敬请期待该系列的后续内容。

至此,在这个讨论 JSR-14 和 Tiger 中泛型类型的微型系列中,我们已经探讨了:

  • 泛型类型及被设计成支持它们的即将发布的功能
  • 基本类型、受约束的泛型以及多态方法上的限制
  • 几个强加给这些 Java 扩展的限制
  • 这些扩展语言的编译器所用的实现策略如何使这些限制成为必需
  • 在泛型类型中添加对“裸”类型参数的 new 操作的支持所带来的影响

本月,我们将探讨在可以处理 mixin(可能被期望是泛型类型中最强大的功能)之前需要先解决的问题,以此来结束对 Java 语言中泛型类型的讨论。

mixin vs 包装

mixin 是由其父类参数化的类。例如,请考虑以下这个泛型类,它继承了它本身的类型参数:

class Scrollable<T> extends T {...}

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 对象,编码了其水平和垂直尺寸。现在,我们假设类 MyTextPaneJComponent 的子类)也包含不带任何参数的方法 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 包含有这样的观点:确定在名称不一致的情况中要调用哪个方法。

在 mixin 作为泛型类型的情况中,我们可以应用同样的解决方案。我们只要设计一些 观点,这些观点在泛型类型的上下文中有效,并且还允许向后兼容 JVM。在 Rice JavaPLT 实验室中,我们已经在“A First-Class Approach to Genericity”(请参阅 参考资料)一文中提出了这样一种解决方案。

有得必有失

正如示例、问题和可能的解决方案所演示的,在 Java 编程中继承泛型类型以包含对 mixin 的支持会产生一种功能强大的语言,但同时也引入了一些有待克服的问题。这是典型的编程语言设计:只能通过使许多现有功能变复杂才能添加所希望的功能。在编程语言领域中,没有任何免费的午餐。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 参加有关文本的 论坛。(您也可以单击本文顶部或底部的 讨论来访问该论坛。)
  • 阅读由 Eric Allen 编写的完整 诊断 Java 代码系列
  • 针对继承 Java 泛型以包含 mixin 所产生的许多问题的详细分析以及提出的解决方案,请参阅由 Allen、Bannet 和 Cartwright 编写的“ A First-Class Approach to Genericity”(PDF)。
  • 在作者于 2002 年 4 月发表的专栏文章“ 连续初始化器错误模式”中了解包含外部不带参数的构造函数实践如何会产生问题。
  • 通过下载 JSR-14 原型编译器,快速获取有关 Java 中泛型的信息;它包含用扩展语言编写的原型编译器的源代码、一个包含类文件的 JAR 文件(用于运行并引导该编译器)以及一个包含集合类存根的 JAR 文件。
  • 您可以马上下载 NextGen原型编译器。
  • 查阅 DrJava,这是一个免费的 Java IDE,它支持 Java 语句和表达式的交互式求值,并支持泛型 Java 语法和编译。
  • 通过阅读 Java 社区过程(Java Community Process)的建议 JSR-14来参加将泛型类型添加到 Java 的讨论。
  • Keith Turner 编写的“ 编译时使用 Generic Java 捕获更多的错误”( developerWorks,2001 年 3 月)一文从另一个角度讨论了该主题。
  • IBM Research 的“ Automatic Code Generation from Design Patterns”一文(PDF)描述了自动实现设计模式的工具的体系结构和实现。
  • developerWorksJava 技术专区 查找数百篇 Java 技术参考资料。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=54715
ArticleTitle=诊断 Java 代码: 轻松掌握 Java 泛型,第 4 部分
publish-date=07212003