在与 Martin Fowler 共同参加的一次主题演讲中,他提供了一个敏锐的观察报告:
Java 的遗产是 平台,不是 语言。
最初的 Java 技术工程师曾做过一个了不起的决定,将语言从运行时中分离出来,最终使 200 多种语言可在 Java 平台上运行。该基础架构对平台保持长久活力非常关键,因为计算机编程语言的寿命通常很短。自 2008 年以来,每年由 Oracle 主办的 JVM 语言峰会都会为 JVM 上替代语言的实现者提供与平台工程师公开合作的机会。
欢迎来到 Java 下一代专栏系列。在这里,我将简要介绍三种现代 JVM 语言:Groovy、Scala 和 Clojure,它们将范式、设计选择和舒适因素进行了有趣的组合。我不打算在这里详细介绍每种语言;它们各自的网站上都有这方面的介绍(参阅 参考资料)。但语言社区网站(主要目的是福音传道)上没有提供语言不适应的客观信息或任务示例。在本系列文章中,我将进行实质性对比,帮助填补这项空白。本文准备概述 Java 下一代语言以及学习这些语言的好处。
Java 语言因 Bruce Tate 在其著作 超越 Java(参阅 参考资料)中将其称为 完美风暴而出名,导致 Java 出名的综合因素包括:Web 的兴起、现有 Web 技术因各种原因产生的不适应性,以及企业多层应用开发的兴起。Tate 也认为完美风暴是一系列独立事件,而其他语言不会以同样方式达到同样的高度。
Java 语言已证明其功能相当灵活,但人所共知,其语法和固有范式具有一定的局限性。尽管 Java 语言正在进行一些看似美好的改变,但其语法根本不支持一些重要的未来目标,如函数式编程元素。但是,如果您试图找到一种新语言取代 Java,那您就错了。
多语言编程是我在 2006 年的一片博客文章中重新提出并推广的一个术语(参阅 参考资料),多语言编程以单一语言并不适合解决所有问题的认知为基础的。一些语言具有更适合某些特定问题的内在特性。例如,虽然 Swing 与 Java 一样成熟,但开发人员发现在 Java 中编写 Swing UI 非常麻烦,因为它要求进行类型声明,要求行为具有匿名内部类,并且具有其他冲突因素。使用更适合构建 UI 的语言,比如带有SwingBuilder的 Groovy(参阅 参考资料),就会使构建 Swing 应用程序变得更容易。
JVM 上运行的语言的扩展使多语言编程的构思更具吸引力,因为您可以在维护相同的底层字节码和库时将其混搭。例如,SwingBuilder不能取代 Swing;它在现有 Swing API 上进行分层。当然,长期以来,开发人员一直混合使用 JVM 以外的语言(例如,混搭使用 SQL 和 JavaScript 来实现特定目的),这在 JVM 范围内更加普遍。许多 ThoughtWorks 项目包含多种语言,ThoughtWorks Studios 开发的所有工具均使用混合语言。
即使 Java 依旧是您的主要开发语言,也可以了解如何运行其他语言,以便在策略上使用它们。Java 依然是 JVM 生态系统的重要部分,但最终人们更倾向于将它用作平台汇编语言 —一个您可以完全了解性能或满足特定需求的地方。
20 世纪 80 年代初,在我上大学期时,我们使用称为 Pecan Pascal 的开发环境。它的独特之处是可以同时在 Apple II 或 IBM PC 上运行相同的 Pascal 代码。Pecan 工程师使用某种称为 “字节码” 的神秘东西实现了这一壮举。开发人员将他们的 Pascal 代码编译成这种 “字节码”,并在为每个平台本地编写的 “虚拟机” 上运行。多么可怕的经历啊!甚至对于简单的任务而言,生成代码也极其缓慢。当时的硬件根本无法应对这种挑战。
发布 Pecan Pascal 之后的十年,Sun 发布了 Java,Java 使用了相同的架构,对于 20 世纪 90 年代中期的硬件环境,运行该代码显得有些紧张,但最终取得了成功。Java 还增加了其他开发人员友好的特性,如自动垃圾收集。使用过像 C++ 这样的语言之后,我再也不想在没有垃圾收集的语言中编写代码。我宁愿花将时间花在更高层次上的抽象上,思考解决复杂业务问题的方法,也不愿意在内存管理等复杂的管道问题上浪费时间。
计算机语言通常不会有很长的寿命,原因之一是语言和平台设计的创新速度。随着我们的平台逐渐强大,可以处理的繁重作业也就越多。例如,Groovy 的 备忘特性(2010 年增加的特性)缓冲了函数调用结果。不需要手写缓冲代码,这可能会引入 bug,只需调用 memoize()方法即可,如清单 1 所示:
def static sum = { number -> factorsOf(number).inject(0, {i, j -> i + j}) } def static sumOfFactors = sum.memoize()
清单 1 中,sumOfFactors方法的结果是自动缓存的。您也可以使用另一种方法自定义缓冲行为,比如 memoizeAtLeast()和memoizeAtMost()。Clojure 还提供了备忘功能,这对 Scala 的实现是无足轻重的。下一代语言(以及一些 Java 框架)中的高级特性(比如备忘功能)将逐渐找到它们进入 Java 语言的方法。Java 的下一个版本将增加高阶函数,使备忘功能的实现变得更容易。通过学习下一代 Java 语言,提前了解未来 Java 特性。
Groovy 是 21 世纪的 Java 语法(浓缩咖啡,而非普通咖啡)。Groovy 的设计目标是更新并减少 Java 语法阻力,同时支持 Java 语言中的主要范式。因此,Groovy 需要 “了解” JavaBeans 这类技术,并简化属性访问。Groovy 快速合并新特性,并提供了一些重要函数特性,我将在后面几期中重点介绍。Groovy 在根本上依然是面向对象的命令式语言。Groovy 与 Java 的两个主要区别是,Groovy 是 静态而非动态类型,而且它的元程序功能更佳。
Scala 是一种充分利用了 JVM 优势的语言,但其语法完全进行了重新设计。Scala 是一种强静态类型语言(比对类型要求比较严格的 Java 更严格)支持面向对象范式和函数范式,而且更青睐于后者。例如,Scala 倾向 val声明,并使不变的变量(类似于在 Java 中将参数标记为final)服从于 var,这创建了人们更加熟悉的可变变量。通过大力支持这两种范式,Scala 为您提供了从您可能是(一名面向对象的命令式程序员)到可能应该是(一名倾向函数式的程序员)的桥梁。
Clojure 是一种 Lisp 方言,在语法上彻底背离了其他语言。它是一种强动态类型语言(和 Groovy 一样),反映了专断的设计决策。虽然 Clojure 允许您用遗留 Java 进行完整和深入的交互操作,但它并不试图构建与旧式范式相连的桥梁。例如,Clojure 不具备纠错功能,并且支持面向对象进行交互操作。但是,它还支持对象程序员所习惯的所有特性,如多态性,但它以函数方式而非面向对象的方式来实现这些特性。Clojure 围绕一些核心工程原理(比如 Software Transactional Memory)进行设计,它打破了旧的范式,支持新的功能。
除了语法之外,这些语言最有趣的不同之处在于类型和底层主要范式:函数式或命令式。
编程语言中的 静态类型指的是显式类型声明,如 Java 的 int x;声明。动态类型指的是不要求提供类型声明信息的语言。所有语言都是 强类型语言,这意味着您的代码可以反映赋值后的类型。
Java 的类型系统饱受责备,因为它的静态类型带来了许多不便,而且无法获得较大收益。例如,在现行的有限类型推断之前,Java 要求开发人员在赋值等号的两侧重复进行类型声明。Scala 与 Java 相比是更加静态的类型,但在日常使用时并不麻烦,因为它很好地利用了类型推断。
Groovy 有一个行为,乍一看,该行为似乎架起了静态与动态之间的桥梁。请考虑清单 2 所示的简单集合工厂:
class CollectionFactory { def List getCollection(description) { if (description == "Array-like") new ArrayList() else if (description == "Stack-like") new Stack() } }
清单 2 中的类充当了一个工厂,它将根据传递的 description 参数返回两个 list 接口实现程序中的一个(ArrayList 或 Stack)。对 Java 开发人员而言,这似乎能够确保返回的结果满足规则。但是,清单 3 中的这两个单元测试显示了它带来的并发症:
@Test void test_search() { List l = f.getCollection("Stack-like") assertTrue l instanceof java.util.Stack l.push("foo") assertThat l.size(), is(1) def r = l.search("foo") } @Test(expected=groovy.lang.MissingMethodException.class) void verify_that_typing_does_not_help() { List l = f.getCollection("Array-like") assertTrue l instanceof java.util.ArrayList l.add("foo") assertThat l.size(), is(1) def r = l.search("foo") }
在 清单 3 的第一个单元测试中,我通过工厂检索 Stack,验证它确实是 Stack,然后执行 Stack的操作,如 push()、size()和 search()。但在第二个单元测试中,我必须用 MissingMethodException 预期发生的异常,以保护测试,这样才能通过测试。在检索 Array-like 集合并将它作为一个 List 放入变量类型时,我可以证明我确实收到了一个 list。但是,当我试图调用这个 search()方法时,它会触发异常,因为ArrayList不包括 search()方法。因此,通过提供无编译时间保护的声明,可以确保方法调用是正确的。
虽然这看起来可能像一个 bug,但它却是正确的行为。Groovy 中的类型只确保 赋值语句的有效性。例如,在 清单 3 中,如果我返回一些 List接口的内容,则会触发运行时异常(GroovyCastException)。这会让 Groovy 与 Clojure 在强动态类型家族中牢牢站稳脚跟。
然而,语言最近发生的变化已使 Groovy 中的静态和动态区分变得十分模糊。Groovy 2.0 新增了一个 @TypeChecked注释,使您能够在类或方法级别上制定严格类型检查特别决策。清单 4 举例说明了这个注释:
@TypeChecked @Test void type_checking() { def f = new CollectionFactory() List l = f.getCollection("Stack-like") l.add("foo") def r = l.pop() assertEquals r, "foo" }
在 清单 4 中,我添加了 @TypeChecked 注释,验证了赋值和随后的方法调用。例如,清单 5 中的代码不再进行 编译:
@TypeChecked @Test void invalid_type() { def f = new CollectionFactory() Stack s = (Stack) f.getCollection("Stack-like") s.add("foo") def result = s.search("foo") }
在 清单 5 中,我必须为工厂返回添加类型转换,以支持我调用 Stack的 search()方法。该工具与限制条件一起使用:在启用静态类型时,Groovy 的许多动态特性是无效的。但是,该示例说明了 Groovy 在建立静态和动态划分之间的桥梁时的不断变化。
所有这些语言都是十分强大的元程序编程工具,所以事后可以添加更加严格的类型。例如,有一些辅助项目可以将选择类型添加到 Clojure。但是,通常情况下,如果选择类型是可选的,那么它就不再是类型系统的一部分;它是一种验证机制。
另一个主要比照是命令式与函数式。命令式编程主要关注分步指令,在许多情况下,模仿古老的低级硬件的便利条件。函数式编程更关注一流结构函数,并试图最大程度地减少状态转换和易变性。
受 Java 影响的比较大的 Groovy 实际上是一种命令式语言。但从一开始它就包含大量函数式语言特性,随着时间的推移,还在不断添加新的特性。
Scala 将这两种范式结合起来使用并为它们提供支持。虽然函数式编程更受喜欢(并受支持),但 Scala 仍然支持面向对象的编程和命令式编程。因此,恰当使用 Scala 要求一个纪律严明的团队确保您不会将范式随意混合搭配,这在多范式语言中一直是一种危险举动。
Clojure 没有提供纠错功能。它支持面向对象的编程,以便支持与其他 JVM 语言轻松交互,但它并不会充当桥梁。相反,Clojure 的专断设计声明设计师所思考的正是良好的工程实践。那些决策影响深远,使 Clojure 能够以开创性的方式解决 Java 世界冗繁的问题(如并发性)。
了解这些全新语言所需的诸多思维转换都来自命令式 / 函数式划分,而且这是本系列文章中最有意义的探索领域之一。
开发人员生活在一个采用多种语言解决问题的语言种类越来越多的世界中。学会有效使用新语言有助于您确定某种方法何时适用。您无需抛弃 Java,它将逐渐融合下一代 JVM 语言的特性;通过了解它们,您可以窥视 Java 语言的未来。
在下一期的 Java 下一代语言中,我将开始对比 Groovy、Scala 和 Clojure 之间的共同之处。