在不改变语言的前提下如何推进 Java 的不断演进

阅读数:4354 2010 年 3 月 16 日

James Gosling 在“The Feel of Java”中说到:

Java 是一种蓝领语言,它并非博士的论文素材而是用于完成工作的语言。各式各样的程序员都非常熟悉 Java,因为在设计 Java 之初我就坚持这样一种观点:选择久经考验的东西而非仅仅是听起来很美。

Java 所获得的巨大成功证明了这种设计方式是正确的,但如果这依然是当今 Java 的重要目标的话,那么其结果就是语言的演进将变得非常缓慢。除此以 外,Java 是一门成熟、使用广泛的语言这个事实也将导致其演进过程充满了困难。一方面,添加到语言中的每个特性都可能在一定程度上造成不可预料的结果, 这么做会疏远那些已经使用了该语言的开发者。另一方面,本身很完美的特性可能在同语言中的其他特性进行交互时产生不可预料的结果。更糟的是,一旦增加了某 个语言特性,几乎就不可能再将其移除了,即便是该特性会对整个语言产生不利影响也没办法。为了证明某个新特性是正确的,语言设计者必须确信从长远来看,该 特性会给语言带来好处,而不是短期效益或是针对某个问题的快速解决方案,之后就变得可有可无了。为了降低风险,语言设计者通常都会创建单独的一种语言或是 分支来进行试验,比如 Pizza 语言就是在实现前用来测试 Java 泛型的。这种方式的问题在于试验的参与者非常小众并且都是自己想参与进来的;显然他们对 语言特性很感兴趣,很多人都是学者或研究员。但是,在普通的程序员开始使用这些特性时,那些学者或是研究员认为很棒的特性可能会变得很糟。

为了直观感受一下这种情况,请考虑关于 Java 7 闭包特性的激烈争论。一段时间以来,有人在提案中给出了闭包的实现,但最终却还是没有达成共识。随后,Sun 决定不打算在 JDK 7 中添加完整的闭包支持。这时争论的焦点转向为 Java 是否变得越来越复杂了,在 Java 5 中添加泛型(尤其是通配符语法)时就已经出现了这种争论;在 Java 已经通过匿名内部类部分实现该功能的情况下,完整的闭包支持是否是正确的呢。需要完 整闭包支持的两个重要场景是简化 fork/join API(添加到了 JDK 7 中以改进多核编程)的使用以及辅助资源的清理。Josh Bloch 的 ARM block 提案(期望通过 Project Coin 加入到 JDK 7 中)就第二个问题给出了另一种解决方案。Cliff Click 博士在面向 Java 的可扩展、非阻塞编程风格的研究中给出了关于 fork/join 的另一种方案,随着处理器核心数的不断增 加,这种方案看起来更合理。如果这一切都成为可能的话,那么 Java 中使用闭包的地方将变得非常少了,语言根本没必要提供这个特性。

话虽如此,但对于编程语言来说,持续不断地平稳发展还是非常重要的。因此本文探究了如下 3 种技术以向 Java 中增加新的语言特性而又不改变语言本身,他们 是客户化领域特定语言(DSL)、Java 6 的注解处理器(通过库来增加可选的语言特性)以及将语法糖从语言迁移到 IDE 中。每项技术都可以让众多的主流开发者以非侵入的方式体验这些新特性,最棒 的想法则可以融入到语言核心当中。

客户化 DSL

在这 3 项技术中,人们谈论最多的还是DSL。该术语的确切含义至今尚未统一,但出于讨论的目的,我们在这里简单地把它看作是用于解决特定问题、应用范围很窄 的一种语言而非用于解决所有计算问题的通用语言。这样,DSL 就并非是图灵完备(non-Turing complete)的。当然了,还是会有一些边际情况存在的,比如说Postscript是一种图灵完备的语言, 但根据我们方才的定义,它也是一种 DSL。

如上所述,DSL 并非新概念。其他类似的 DSL 还有正则表达式、XSLT、Ant 以及 JSP 等等,所有这些都需要某种客户化的解析器对其进行处理。Martin Fowler 还说fluent interfaces/API 也可以看作是另一种 DSL,称之为内部 DSL。他说内部 DSL 是直接在宿主语言中开发出来的。这对于 Lisp 和 Smalltalk 开发者来说很容易理解,而最近 Ruby 社区也开始对内部 DSL 情有独钟了。

虽然很多知名的 DSL 都是由商业公司开发和维护的,但一些企业开发团队也已经使用该技术来创建能够快速解决其问题的语言了,但毕竟还是小众,这可能是 DSL 领域门槛比较高的缘故吧。负责 DSL 的团队必须要设计语言、构建解析器和其他工具来支持开发团队,还要对每个新员工进行培训,让其了解 DSL 的工作 机理。这时,涌现出了能够支援 DSL 开发的工具,这极大地改变了当前的状况。Intentional Software 所开发的Intentional Domain Workbench比 Java 还要久远,它首度实现了该工具的功能。该项目创建于微软研究院,Charles Simonyi 博士在 1995 年所发表的论文“The Death of Computer Languages, the Birth of Intentional Programming”描 绘了其愿景。2002 年,Simonyi 创建了 Intentional Software 以继续实现其想法,大家可以看看介绍该系统的视频,极具震撼力。目前该产品的版本是 1.0,但只有极少数的合作者能够访问。

其他一些软件公司也在研究这个概念,其中就包括以IntelliJ IDEA Java IDE而扬名天下的 JetBrains,JetBrains 最近发布了 Meta Programming System(MPS)1.0 版。MPS 并没有使用解析器而是直接使用了 Abstract Syntax Tree(AST)。它提供了一个类似于文本的投影编辑器(projectional editor)以便程序员能够操纵 AST,同时该编辑器也可用于编写语言和程序。当程序员使用投影时就会为树上的每个节点创建一个文本投影,这样变换就会 反映到节点当中。开发者可以通过这种方式以任意组合(通常称之为语言组合)扩展和嵌入语言。JetBrains 正在内部使用该产品,最近发布的 bug 追踪 产品YouTrack就是使用该系统开发的。

Java 6 注解处理器

相对于 Ruby、Smalltalk 和 Lisp 来说,DSL 在很多主流语言(如 Java)中的流行程度就稍逊一筹了,但最近 Java 语言的一些变化(尤其 是 Java 6 中新增的注解处理器)为开发者提供了新的机遇以在其中使用 DSL。对于 Java EE 6 中的JPA 2.0来说,其某些 API 本身就是 DSL。注解处理器会为应用中的每个持久化类建立一个元模型类型(metamodel type)。虽然开发者可以手工处理 Java 中的元模型,但这实在太无聊而且极易出错。注解处理器的出现改变了这一切,因为它内建于 Java 6,因此无需特殊的 IDE 支持——IDE 会代理编译器所触发的注解处理器,之后会自动生成元数据模型。

程序库也可以通过注解处理器来提供新的语言特性。比如说,Bruce Chapman 的原型“no closures”提案就凭借该技术将方法转换为 Single Abstract Method(SAM)类型,然后在 Java 6 上编译。在与其交谈过程中,Chapman 指出 SAM 类型还支持自由变量(free variable),这是闭包的一个关键技术:

除了 Single Abstract Method 所需的参数外,方法体还可以通过 @As.Additional 注解声明额外的参数。在获得 SAM 类型的实例时,这些参数可以带有绑定值,然后 在每次调用时传递给方法。

Chapman 还创建了Rapt 项目以探索该技术的其他使用场景,同时为语言的两个变化提供了自己的实现——多行字符串(Multiline Strings)XML 字面值(XML literals)——这两个特性是 为 JDK 7 准备的,但却不会包含到最终的发布中。Java 甚至也可以使用这种方法实现闭包,Chapman 对此说到:

我们刚刚使用该技术完成了一个 Swing 项目,在这个过程中发现了泛型的一些小 bug,其中一个 bug 还没有修复,除此之外一切都很棒,没人再想使用传统 的匿名内部类了。

另一个探究注解处理器的项目是Lombok,它将该技术又向前推进了一大步。Lombok 将注解作为回调以运行 Java agent,后者会根据注解重写各种 javac 内核。由于操纵的是内部类,因此它不太适合于产品使用(JVM 各个小版本中的内部类也可能不一样),但该项 目对于注解处理器到底能做什么这个问题上还是颇具启发意义的,包括:

  • 通过 @Getter 和 @Setter 注解定义各种访问级别的属性,如 @Setter(AccessLevel.PROTECTED) private String name;
  • @EqualsAndHashCode 注解会根据对象中的属性实现 hashCode() 和 equals() 方法
  • @ToString 注解会实现 toString() 方法
  • @data 方法相当于 @ToString、@EqualsAndHashCode、所有属性的 @Getter 以及所有非 final 属性的 @Setter 的集合,可以使用 @data 方法和构造方法初始化 final 属性

还可以通过这种方式进行其他的语言试验,比如移除 Java 中的非运行时异常等。

虽然注解处理器技术为语言试验开辟了一条新航线,但还是要注意生成代码的可读性,保证开发者能读懂生成的代码。Chapman 给出了很多建议:

要生成源代码而不是字节码,注意生成代码的格式(尤其是缩进)。编译器不在乎生成的代码是不是都在同一行,但用户在乎。我甚至还使用注解处理器在恰当的地方增加了一些注释和 javadoc。

如果这项技术逐渐流行起来,用户将可以通过 IDE 查看编译期所生成的代码。

IDE 中的语法糖

Bruce Chapman 还提到了第三项技术——将语法糖从语言迁移到 IDE 中——他在博客中对该问题做过深入阐述。对于 Java IDE 来说,生成部分样板代码已成为不可或缺的功能了,比如类的 getters 和 setters,但 IDE 开发者刚刚开始深挖该这个概念。 JetBrains 的 IntelliJ 9为内部类提供了一个类似于闭包的简洁的代码块语法,开 发者也可以自己加。就像代码折叠一样,该语法会扩展到编译器能够处理的完整的匿名内部类——这样坚持使用标准的匿名内部类语法的开发者很容易就适应了。 Eclipse 也有一个类似的插件。关键在于这种语法仅仅是实际代码的另一种展示方式而已,编译器和任何源代码管理工具都能够像以前那样处 理他们。开发者可以在两种方式间自由切换(就像代码的展开与收起一样),无权访问该语法糖定义的人仅仅会看到正常的 Java 代码。Chapman说到

要想实现易于使用的目标还有很多工作要做,但从长远来看,我发现开发者会很轻松地实现加糖 / 脱糖(sugaring/desugaring)的转换(可以 参考jackpot来 了解实现原理)、不断尝试、不断演进并与同事和社区分享好的点子。这么做的好处与语言的演进别无二致。最好的东西会流行起来并形成实际语言演进的基础,如 果必要的话还可以随时去除该方法无法实现的“噪音”。

由于语法糖还要兼顾(非常多)其他的语言特性,因此无法提供完整的闭包支持;比如说BGGA 闭包就有一些特性无法匹配匿名内部类,因此不能通过这种方式实现。话虽如此,这种想 法却展示了通过各种新语法来表示匿名内部类的可行性,类似于 BGGA 语法或是FCM 语法,开发者也可以选择自己喜欢的语法。其他的语言特性(如 null-safe Elvis operator)可以通过这种方式实现。要想进一步验证该想法,可以体验一下这个NetBeans module(由 Chapman 开发),这正是他所说的用于 Properties 的原型。

结论

在语言的发展过程中总是需要考虑稳定与发展之间的平衡。上面介绍的技术所带来的好处是他们不会影响平台或是语言本身。这么做允许我们犯错误,也有益于进行 快速激进的试验。由于开发者能够自由地进行试验,我们看到越来越多的人开始解决常见的样板代码“噪音”问题,如匿名内部类语法等,同时将这些想法以合理的 方式组织起来以获得最大的价值。我们将欣喜地看到开发者使用这些方法将 Java 平台推向新的远方。

查看英文原文:Evolving Java Without Changing the Language


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论