Java 开发人员和研究人员 Eric Allen 讨论了通过泛型类型添加对 mixin 支持所带来的影响,并以此文来结束他对 JSR-14 和 Tiger 中泛型类型的由四部分组成的系列的讨论。在相关论坛中与作者及其他读者分享您对本文的看法。(您也可以单击本文顶部或底部的“讨论”来访问该论坛。)
至此,在这个讨论 JSR-14 和 Tiger 中泛型类型的微型系列中,我们已经探讨了:
- 泛型类型及被设计成支持它们的即将发布的功能
- 基本类型、受约束的泛型以及多态方法上的限制
- 几个强加给这些 Java 扩展的限制
- 这些扩展语言的编译器所用的实现策略如何使这些限制成为必需
- 在泛型类型中添加对“裸”类型参数的
new
操作的支持所带来的影响
本月,我们将探讨在可以处理 mixin(可能被期望是泛型类型中最强大的功能)之前需要先解决的问题,以此来结束对 Java 语言中泛型类型的讨论。
mixin vs 包装
mixin 是由其父类参数化的类。例如,请考虑以下这个泛型类,它继承了它本身的类型参数:
|
第 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
的子类:
|
那么我们的 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. 意外方法覆盖的示例
|
随后 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 的支持会产生一种功能强大的语言,但同时也引入了一些有待克服的问题。这是典型的编程语言设计:只能通过使许多现有功能变复杂才能添加所希望的功能。在编程语言领域中,没有任何免费的午餐。