1.6 其他JVM语言
为推广Java语言和平台,Sun很早就公布了JVM规范。这个文档旨在供那些要自己动手编写JVM实现的开发人员参考,这些JVM实现可能是为那些没有官方JVM实现的平台编写的。这个文档描述了JVM可执行的低级命令、必须提供的数据结构、内存访问规则、Java字节码文件格式.class等。
这个规范的发布让其他语言的设计者能够尝试Java字节码,因此不久后其他语言也能够编译成这种格式。这虽然不是Java设计者的初衷,但Sun(和后来的Oracle)乐见其成。它们是如此地乐见其成,以至于仅仅为方便JVM支持动态语言而添加了新功能。
本节将介绍与其他JVM语言相关的如下主题。
- 为何要放弃Java转而使用其他语言进行JVM开发?
- 在同一个项目中使用多种语言的可能性以及这样做可能带来的问题。
- 使用不同于主项目使用的语言编写单元测试。
1.6.1 为何选择其他语言
考虑到Java语言最初就是为在JVM上运行而设计的,怎么会有人选择使用其他语言来进行JVM开发呢?
开发人员这样做的原因有多个:
- Java是一种非常繁琐的语言;
- 并非所有人都喜欢静态类型语言,且静态语言并非在任何情况下都是最佳选择;
Java类库缺少一些经常需要用到的类。
Java是一种非常繁琐的语言
Java以繁琐著称。经过多年的修订后,Java已不再那么繁琐,但使用很多其他的语言时,完成同样的任务所需的代码更少。
我们来看一个简单的示例。
在Java中,标准的可修改对象通常类似于下面这样:
class Person {private String name;public Person(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
而使用Kotlin时,只需下面一行代码就能实现同样的功能(以及其他功能):
data class Person(val name: String)
这可不是开玩笑。编译这行代码时,Kotlin将自动实现Java示例中定义的方法;实际上,它还会添加其他较为常用的方法。第4章将讨论这些额外的方法。
虽然使用其他语言可极大地提高效率,但Java也没有看起来那么糟糕。所有现代IDE编程工具都能自动生成Java样板代码,如前述示例中的样板代码。为此,你只需按下一个简单的组合键即可。
- Java并非对所有的人或在任何情况下都是理想选择
虽然Java 8新增了一些重要的函数式编程功能,但从本质上说,它依然是一种静态的命令式语言。并非所有的开发人员都喜欢这种编程风格。如果必须使用静态语言来编写代码,Python或Ruby程序员可能会哭晕在厕所。有鉴于此,有些团队放弃Java转而选择使用其他语言来进行JVM开发。
另外,对于有些问题,使用动态编程语言解决起来要优雅得多;同时对于必须执行复杂并发操作的项目来说,函数式编程风格通常更合适。最后,对于有些哭和框架,通过某些语言来使用让人感觉更为自然。
- Java类库缺失的类
Java类库是一个庞大的库,但它毕竟是20多年前推出的,因此在有些情况下缺少必要的类。在大多数情况下,功能缺失的问题可通过添加JVM生态系统中免费的开源插件库来解决,但如果选择使用内置了这些功能的语言,不仅更方便,还可节省时间。
例如,对于极其常用的JSON标准,Java SE 8的Java类库就没有提供原生支持。流行的插件库Jackson和Google GSON提供了JSON支持;另外,较新的Java EE平台版本也提供了支持JSON的API。本书介绍的一些语言提供了原生的JSON支持。
另一个问题是,要使用Java类库中的一些常用类,必须编写大量的样板代码。对于Java类库中的众多常用类,诸如Groovy等语言都提供了包装器,让这些API使用起来轻松得多。
1.6.2 在同一个项目中使用多种JVM语言
很多语言都能够与Java互操作,因此也能够与其他JVM语言互操作。这是通过尽可能为数据结构使用标准Java类库并像Java那样编译方法来实现的。
在Java项目中,经常使用其他语言来编写某些类,但这样做可能带来一些问题:
- 构建过程将复杂得多;
很多语言要求使用自己的运行时类,这可能带来问题。
构建过程更复杂
使用多种语言时,必须调整构建脚本,这可能导致情况非常复杂。例如,如果Java项目使用了用Groovy编译的类,编译顺序将非常重要,即必须先编译Groovy类,再编译Java代码。如果Groovy代码使用了Java项目中的自定义类,情况将更加复杂。
你将在第11章看到,Groovy比较特殊。Groovy编译器能够编译大部分Java代码,因为从很大程度上说,Groovy语言与Java语言兼容。在没法这样做或这样做不可取时,有一个用于构建工具Apache Maven的编译器插件,使用它可解决构建过程中面临的问题。
一种解决之道是将代码分成多个子项目,并在构建工具中将生成的库作为主项目的需求列出。
有些语言提供了另一种解决方案:它们提供了自定义类,让你能够在Java(或其他JVM语言)中调用这些语言的源代码。被这些类加载时,源代码将动态地编译为Java字节码。其他语言实现了在Java代码中嵌入脚本语言的官方标准;附录A讨论Oracle的JavaScript解释器Nashorn时,将简要地讨论这一点。
- 语言运行时库
从某种程度上说,这与构建并发症相关。很多JVM语言都要求在编译后的程序中包含支持库,这些库通常定义了语言特有的数据结构以及编译得到的Java字节码调用的内部支持方法。
这通常不是问题,但如果项目的依赖项(或依赖项的依赖项)是使用不同的语言版本编写的,就可能出现问题。如果同一个项目的多个库要求使用同一个运行时库的不同版本,情况将更为复杂:编译或运行项目时,可能出现令人费解的错误消息。
这被称为依赖恶梦(dependency hell),但这并非是在同一个项目中使用多种语言特有的现象,而是每个开发人员都应知道的现象。打算在同一个项目中使用多种语言的开发人员还必须明白,语言运行时库可能导致最终程序的规模急剧增大;有些运行时库还包含其依赖项,这将增大出现依赖恶梦的风险。通常,语言的文档或官网都详细说明了其依赖项。
与很多框架设计者一样,很多语言设计者都对这些问题心中有数,并采取了措施降低这些问题出现的风险。例如,对于自己使用的较流行的依赖项,他们进行了重命名,以防发生类名冲突。
1.6.3 使用另一种语言编写单元测试
使用其他语言编写的单元测试来对Java代码进行测试很常见。正如你在本章前面看到的,相比于Java,使用其他语言编写的代码可能紧凑得多,这些语言非常适合用来编写小型、具体而易于理解的单元测试。
鉴于仅在运行单元测试时才会用到这些语言的运行时库,因此无需在编译得到的主项目中包含它们。
你将在第11章看到,在这样的情形下,非常适合使用Groovy。Groovy提供了一些便利的单元测试编写功能,其中包括内置的
assert语句,这种语句在传入的值与期望值不同时将打印非常详尽而易于理解的输出。。
虽然使用其他语言可极大地提高效率,但Java也没有看起来那么糟糕。所有现代IDE编程工具都能自动生成Java样板代码,如前述示例中的样板代码。为此,你只需按下一个简单的组合键即可。