JVM 平台上的各种语言的开发指南 1
为什么我们需要如此多的JVM语言?
在2013年你可以有50中JVM语言的选择来用于你的下一个项目。尽管你可以说出一大打的名字,你会准备为你的下一个项目选择一种新的JVM语言么?
如今借助来自像Xtext和ANTLR这样的工具的支持,创建一种新的语言比以前容易多了。个体编码者和群体受突破和改进现存JVM语言,以及传统Java的限制和缺陷的驱使,让许多新的JVM语言应运而生。
新的JVM语言开发者感觉他们的工作室针对现存语言的产物——现存的语言提供了太过受限制的功能,要不就是功能太过繁杂,导致语言的臃肿和复杂。软件开发 在一个广阔的范围被应用,于是一种语言的有效性就决定于它跟特定任务领域的相关性,或者它如何在更广泛的范围中通用。所有这一切导致了资源库和框架的开发。
- 大部分人大谈特谈JAVA语言,这对于我来说也许听起来很奇怪,但是我无法不去在意。JVM才是Java生态系统的核心啊。
- James Gosling,
- Java编程语言的创造者 (2011, TheServerSide)
如此多的语言存世,语言的坟场是否会饱满呢?这里有一个潜在的风险,市面上可供使用的选择太多,将会导致许多语言由于足够的关注和社区贡献而无法生存发展下去。
然而要在这个行业生存下去,必须基于创新和创造——这些往往来自于一个从零开始,并且放弃现有的抱怨和成见,在一块白板上面起家的项目。
这里有一条我们将自然而然遵循的线索:现有的语言建设和框架帮助建造起来的社区支持了Java生存,并且也使得下一代Java和新的创意、结构和范式,这些东西的产生成为可能,最终将使它们的方式体现在现存的语言当中。
Rebel Labs的报道了概览了Java 8,Scala,Kotlin,Ceylon,Xtend,Groovy,Clojure和Fantom。但是如此多的JVM语言可供选择,我们如何会只看中这8种选择?
Rebel Labs 的团队就如何出这样一份报告,还有选择哪种语言,进行了六个月的讨论。最基本的,我们想要呈现给每个人一些东西:Java是一种极其著名,应用广泛的语 言,但是Java 8拥有许多我们想要一探究竟的新东西。Groovy,Scala和Clojure已经找到了它们在市场中的核心竞争力,并且变得越来越流行起来,而像 Ceylon,Kotlin,Xtend和Fantom在我们的涉猎中相对还比较新颖,需要经受一些考察来获得信任。
我们的目标是建立对每一种语言的认识,它们是如何进化的,未来将走向何方。因此在这份报告中,你将会看到我们阐述对于这些语言的第一印象,包括那些给我们带来震撼的特性,以及不那么能打动人的特性。
你将会看到一个HTTP服务器基础实现的源代码示例,它链接到了GitHub,因此你可以同我们一道来探讨它。
一小段历史
最开始只存在Java,它是用于在JVM上编程的唯一选择。但是这个行业很早就满足了对在JVM上面编程的更多和潜在的选择需求。在脚本领域首先出现了 Jython,JVM的一种Python实现,还有Rhino和JavaScript的JVM引擎,它们出现在1997年,跟着就是2000年的 BeanShell和2011年的JRuby。
由于对于动态定制应用程序的需哟,在那时脚本设施的需求很旺盛。如今,像Oracle WebLogic和IBM WebSphere这些应用服务器都使用Jython脚本来执行自动化操作,而Rhino也被绑定在Java 6上面,使得JavaScript成了JVM上的一等公民。
然而,脚本设施不是唯一一个让基于JVM的可选编程语言滋生的因素。归因于Java的向后兼容性原则,为了提供一些Java或者它的标准库没有提供的新颖特性,可选语言开始出现了。Scala和Groovy就是最先提供了一些东西超越了Java的成功项目.
我们可以观察到一种有趣的现象:大部分新晋的编程语言都利用了静态类型。使用Scala,Ceylon,Xtend,Kotlin和Java本身的开发者 都依赖于在编译时验证目标类型。Fantom在动态和静态类型之间找到黄金的平衡中点,而Groovy,尽管一开始是一种动态语言,但是如今也在其 2012年的2.0发行版中也开始加入编译时的静态类型检查了。Clojure——有点Lisp的味道——是坚持使用动态类型,但唯一还收到合理拥泵的 JVM编程语言,然而一些在企业的大型团队中工作的开发者择认为这会是Clojure的一个缺陷。
运行在JVM上的新的编程语言,已经有从定制化应用程序的动态脚本语言,向着一般意义的静态的应用程序开发语言改变的趋势。
Java仍然是最常使用在JVM上的编程语言,而随着Java 8发行版的到来,Java将尝试在语法美学和多核编程方面,跟上时代的潮流。
在 Github Repo 上代码样例
在几个JVM语言的引擎下这会变的很geek。 在这篇文章中,我们从新的角度看Java(换句话说, 在Java 8中), Scala, Groovy, Fantom, Clojure, Ceylon, Kotlin 和Xtend–mostly, 并且给出最吸引我们和我们最深刻的印象。
每一个语言都有自己的 HTTPServer 样例 ,它们都在 github 上。你可以检查我们的代码,所有在这篇文章的JVM 语言 都在这:
https://github.com/zeroturnaround/jvm-languages-report
JAVA 8
“我真正关心的是Java虚拟机的概念,因为是它把所有的东西都联系在了一起;是它造就了Java语言;是它使得事物能在所有的异构平台上得到运行;也还是它使得所有类型的语言能够共存。”
James Gosling,
Java编程语言的创造者 (2011, ServerSide)
Java 8 入门
JavaSE 8.0是值得期待的。
让我们来看一看Java平台的总体演变策略:
- 不去打破二进制代码的兼容性
- 避免引入源代码级的不兼容
- 管控行为方式级别的兼容性变更
简单来说,目标就是保持现有的二进制文档能够链接并运行,并且保持现有的源代码编译能够通过.向后兼容的政策已经影响到了Java这种语言的特性集,同时 也影响到了这些特性如何被实现.例如,使用目前的Java特性不可能促进API的进化,因为变更接口可能会打破现有依赖于JDK接口的资源库,其源代码的 兼容性.这就产生了一个同时影响到语言和JVM的改变.
随着Jigsaw——模块化的主题——正从Java 8中取缔,Lambda项目成为了即将到来的发行版中最重要的主题.尽管其名号有一点点误导性.但是lambada表达式确实是其一个重要的部分,它本身并不是什么重要的特性,但却是Java在多核心领域要做出努力的一个工具.
这个多核心的时代有对于并行库的需求,并且也对Java中的集合(Collection)API造成了压力.这下就需要lambda表达式使得API更加友好和易于使用.防御式方法是API革命的工具,并且也是现存的集合库将如何向支持多核迈出步伐的基础.
那么你想要使用lambda,嗯?
如果你熟悉其它包含lambda表达式的语言,像Groovy或者Ruby,你将会惊喜与它在Java中是如此简单.在Java中,lambda表达式的作用表现在"SAM类型"——一个拥有抽象方法的接口(是的,接口现在可以包含非抽象的方法了——叫做防御方法)。
那么举个例子来说,著名的的Runnable接口可以完美地适合于作为一个SAM类型提供出来:
Runnable r = ()-> System.out.println("hello lambda!");
这也同样适用于Comparable接口:
Comparator<Integer> cmp = (x, y) -> (x < y) ? -1 : ((x > y) ? 1 : 0);
同样也可以像下面这样写:
Comparator<Integer> cmp = (x, y) -> {
return (x < y) ? -1 : ((x > y) ? 1 : 0);
};
这样就看起来似乎像是一行lambda表达式拥有隐式地语句返回了.
如果你想写一个能够接受lambda表达式作为参数的方法该怎么做呢?那么你应该将这个参数声明为一个功能的接口,然后你就能够把lambda传进去了。
interface Action { void run(String param); } public void execute(Action action){ action.run(); }
一旦我们拥有了一个将功能接口作为参数的方法,我们就可以像下面这样来调用它了:
execute((String s) -> System.out.println(s));
同样的表达式可以用一个方法引用来替换,因为它只是一个使用了相同参数方法调用。
execute(System.out::println);
然而,如果在参数在进行着任何变化,我们就不能使用方法引用,而只能使用完整的lambda表达式了:
execute((String s) -> System.out.println("*" + s + "*"));
这里的语法是相当漂亮的,尽管Java本身没有功能(functional)类型,但是现在我们已经拥有了一个优雅的Java语言的lambda解决方案。
JDK 8中的函数型(Functional)接口
如我们所了解到的,一个lambda在运行时的表现是一个函数型接口(或者说是一个“SAM类型”),一种只拥有仅仅一个抽象方法的接口。并且尽管JDK 已经包含了大量的接口,像Runnable和Comparable——符合这一标准,对于API的革命来说还是明显不够用的。而在整个代码中大量使用 Runnable,也可能不怎么符合逻辑。
JDK 8中有一个新的包——java.util.function——包含了许多应用于新型API中的函数型接口。我们不会在这里将它们全部列出来——你自己有兴趣的话就去学习学习这个包吧:)
由于一些接口的此消彼长,看起来这个资源库正在积极的进化中。例如,它曾经提供了 java.util.function.Block类,但是在我们写下这份报告时,这个类型却没有出现在最新的构建版中了:
anton$ java -version openjdk version "1.8.0-ea" OpenJDK Runtime Environment (build 1.8.0-ea-b75) OpenJDK 64-Bit Server VM (build 25.0-b15, mixed mode)
如我们所发现的,它已经被Consumer接口替代了,并且被应用于集合资源库中的所有新方法中。例如,Collection接口中像下面这样定义了 forEach 方法:
public default void forEach(Consumer consumer) for (T t : this) { consumer.accept(t); } }
如我们所发现的,它已经被Consumer接口替代了,并且被应用于集合资源库中的所有新方法中。例如,Collection接口中像下面这样定义了 forEach 方法:
public default void forEach(Consumer consumer) for (T t : this) { consumer.accept(t); } }
Consumer 接口的有趣之处在于,它实际上定义了一个抽象方法——accept(T t),还有一个防御型的方法—— Consumer chain(Consumer consumer).。这意味着使用这个接口进行链式调用是可能的。我们还没有在JDK的资源库中找到 chain(...) 方法,因此还不怎么确定它将怎样被应用。
而且,请注意所有的接口都标记上了@FunctionalInterface(http://download.java.net/jdk8/docs/api/java/lang/FunctionalInterface.html)运行时注解。但是除了它在运行时通过注解用javac去确认是否真的是一个功能型接口以外,它里面就不能有更多的抽象方法了。
因此,如果你编译下面这段代码:
@FunctionalInterface interface Action { void run(String param); void stop(String param); }
编译器将会告诉你:
java: Unexpected @FunctionalInterface annotation Action is not a functional interface multiple non-overriding abstract methods found in interface Action
而下面这段代码将会正常的编译:
@FunctionalInterface interface Action { void run(String param); default void stop(String param){} }
防御方法
出现在了 Java 8 中的一个新概念是接口中的默认方法。它意味着,接口不仅可以声明方法的签名,也还可以保持默认的实现。对于这个功能的需求源于对于JDK API中的接口进化需要。
防御方法最显著的应用是在Java的Collection API。如果你使用过Groovy,你可能写过像下面这样的代码:
[1, 2, 3, 4, 5, 6].each { println it }
而如今,我们使用像下面这样的for-each循环来进行迭代操作:
for(Object item: list) { System.out.println(item); }
能够使用这个循环可能是因为 java.util.Collection 接口扩展了 java.util.Iterable 接口,这个java.util.Iterable接口只定义了一个Iterator iterator()方法。要是想Java 利用Groovy类型的迭代,我们就需要Collection和Iterable中都有一个新的方法。然而,如果添加了这个方法,就将打破现有集合资源库 的源代码级别的向后兼容性。因此,在Java 8中,java.util.Iterable 添加了forEach方法,并且为它提供了默认的实现。
public interface Iterable<T> { Iterator iterator(); public default void forEach(Consumer consumer) { for (T t : this) { consumer.accept(t); } } }
添加新的默认方法并没有打破源码级别的兼容性,因为接口的实现类并不需要提供它们自己对于这个方法的实现,因此从Java 7切换到Java 8以后,现有的代码还能继续通过编译。如此,在Java8我们能够像下面这样编写循环代码:
list.forEach(System.out::println);
forEach方法利用了一个功能性接口作为参数,因而我们能够将一个lambda表达式作为一个参数传递进去,也或者可以像上面的代码示例一样是一个方法引用.
这种方法对于多核场景的支持是很重要的,因为使用这种方式你自信的忽略掉循环的细节原理而专注于满足真正的需求——你所依赖的资源库帮助你打理了循环的细节。新的lambda表达式本身对Java开发者并没有多少意义可言,因为没有集合资源库合适的API,使用lambda表达式不太可能能够充分的满足开发者们。
我们询问了不同JVM语言的创建者和项目领导人对于Java8中新特性的看法
SVEN EFFTINGE——XTEND
是的,它们是完全必要的,并且是朝着正确方向的一个良好开端.Xtend将使用Java8作为可选的编译目标,从何生成的代码得到改善.
Java8的lambda表达式同Xtend中的lambda表达式在语义上非常类似.新的流API能够毫无麻烦的同Xtend良好工作.事实上,它在Xtend上比在Java8上面工作得更好.关于这个我已经写了一篇文章[http://blog.efftinge.de/2012/12/java-8-vs-xtend. html]:-).相较于Java8的流(stream)API,我仍然更倾向于选择Guava API,因为它们更加方便而且可读性更高。
防御方法也是一个不错的东西,尽管我不喜欢使用'default'关键字这种语法.他们挣扎过接口和类方法不同的默认可见性.我想他们是在尝试获得一种明显的语法上的区别,以便人们不再混淆类和接口.Xtend中雷和接口的默认可见性是相同的,在这儿那将不是问题.
BRIAN FRANK – FANTOM
可能挺激动的:-) 多年以后,如今实际上每一个现代的语言都已经有了基本的函数式编程机制.然而仍旧有大量的使用着Java的程序员不知道这些概念的,因此我想向更多的开发 者灌输更多的函数式编程风格将会是有益的.我们不认为函数式编程时灵丹妙药,但是确实是工具箱中很给力的一种工具.
GAVIN KING – CEYLON
Java8在某种程度上重新点燃了开发者对Java平台的兴趣,使他们回归Java,那对于Ceylon和其他基于JVM的语言来说是非常美好的事情.
Java8使得一大堆常规的编程任务执行起来更加的方便.当时,从另外一方面来看,经过多年的尝试,Java SE的团队仍然没有推出内置的模块化功能,这令我极其失望.他们在Java8上所作的良好工作绝对值得赞扬,然而失足于这样一个关键之处,给予同样的批 评,我想才是公平的.Lambda是使用和方便的语法糖.但是模块化才是有关Java一切的关键之处,并且是它在某些领域失败的关键原因.
JOCHEN THEODOROU – GROOVY
说我不兴奋,确实.里面有很多Groovy已经实践了很多年的东西.防御方法在我看来就像个半拉子步调,还有就是我不怎么喜欢这样使接口 混淆.Lambda表达式对我来说更加有意思,但是我发现Groovy的闭包(Closure)是更加强大的概念。 Lambda表达式确实能够使Java成为一门更好的语言,但是它们也会使得Java的一些明智的概念复杂化.我想当确定,如果没有Scala和 Groovy的话,这些特性也许永远不会出现.它们(指的是这些特性)是Java对来自众多有竞争力的其它可选JVM语言的压力而做出的反应。而且,它们 也不得不在保持领头羊地位和吸引高级用户之间保持复杂的平衡.因而它们被困在了中间的某个地方,随之产生了lambda表达式和防御方法.
GUILLAUME LAFORGE – GROOVY
这里我并不像Jochen那样消极,尽管在其他语言如何影响Java朝那条路发展这一点上,他的观点同实际情况相差并不远.
虽然Java8的lambda表达式、推倒重来的"流(Stream)"式集合或者防御方法实际上并不如我们所想象的那样,但是我认为所有那些东西结合起来应该能够给开发者们进化他们的API带来一些新的东西,并且它应该有希望更好的设计和精简新老框架的使用.所以我想,总体观之,这对于Java来说是好事.
ANDREY BRASLAV – KOTLIN
Java变得更好意味着千万开发者变得更加快乐.Kotlin能够使他们中的一些人更加的快乐,但这是另外一码子事了:)
只需要"使用闭包(clusure)的Java"的人们将会在Java8中获得它(指闭包),并且会很高兴。但是还有另外一些人,他们不仅仅只需要匿名函数(那也确实是非常重要的,但是整个世界可不止于此哦).
Scala
“意在使其端正,而不塞入太多的语言特性到其里面,我在Scala上专注于使它变得更加的简单.那是人们常常有的一种误解,他们认为Scala是一种带有 许许多多特性的宏大语言.尽管这通常不是真的.它实际上是一个相当小的语言——当Java8面世之时它将比Java更加的小巧。”
Martin Odersky,
Scala 创始人
Scala入门
同本报告中涵盖的大部分语言相比,Scala是相当行之有效的.2003年已经有了它的第一个发行版,但是从2006年才开始出现在许多雷达(radar)上,当时其2.0版本在EPFL上发行了.从那时起它就日益普及,并且有可能接近于与一线语言为伍了,你可以参考语言排行榜( language ranking )来确信这一点.
从2006年开始,它的许多表现令其变得有趣起来——就是使用类型推断混合了面向对象编程(OOP)和函数式编程(FP:Funcitional Programming)的一种静态类型;虽然不是原生的,但是被编译成了高效的代码。它拥有模式匹配、带底层类型的先进类型系统、代数数据类型、结构类 型甚至依赖类型。它也使得表达像单子(monad)这样的分类理论抽象变为可能,但是你可以在自己心里决定是否去在意这个东西。及时你在标准库中使用了一 些单子,但是甚至你也能够在不知道单子是什么的时候那样做。
世上可没有什么典型的Scala开发者这一说——一些使用Scala的人是Java开发者,他们想要拥有更具表达能力的语言,还有一些是函数式编程者,他 们发现了这是一种在JVM上使用的上佳语言。这意味着Scala程序能够被编写成许多完全不同的风格——纯函数式风格的,势必不纯函数式的,或者两者的混 合风格。你甚至可以交叉使用这些风格,使用尽可能抽象的方式,利用先进的类型系统(见 Scalaz&Shapless 资源库,或者只比在你会在Java代码中使用的更多一点点的抽象。
从2006开始,Scala已经经历了一些重大的变化。其中最大的一个变化是Scala2.8中经过大大调整的集合API,它可能是任何语言中最强大的 了,但是相比于大多数的集合库,其实现细节也具有更多的复杂性。版本2.9添加了并行集合(parallel collection),而版本2.10带来了一大堆特性,其中一些是实验性质的:卫生宏( hygienic macros)、新的反射库、字符串差值(String interpolation)、大大增强的模式匹配代码生成器,还有更多的其它特性。
同Java的主要区别
在使用Scala实现的HTTP服务器(HTTPServer)样本中,我们能够马上注意到它摒弃了static关键字,而不像Java中(这个HTTP服务器)将会是一个伴随/单例(Companion/Singleton) 对象中的静态成员。一个伴随(companion)对象是一个与类同名的对象。所以实际上,我们将我们的HttpServer切分成了一个对象和一个类 ——对象拥有静态的部分,而类拥有动态的部分。这些东西在Scala中是两个不同的命名空间,但是为了方便我们能够引入动态部分中的静态命名空间:
mport HttpServer._ // import statics from companion object
Scala允许在任何地方使用引入(import)语句,你可以从对当前范围可见的拥有成员的任何地方,将成员引入到当前范围之中;因此Scala代码中 的这个结构(指import)时间上比Java更加的常用,而Java只允许你将引入语句放在文件的开头,并且只能引入类或者类的静态成员。
与Java比较,还有一个鲜明出众的地方,那就是方法使用def name(argument:Type)的形式定义,并且变量也是如此定义:
val name: Type = initializer // final “variable”
或者是:
var name: Type = initializer // mutable variable
你应该不会去选择使用可变的变量,所以默认使用val吧——我们的样本代码中并没有任何使用var的定义,因为实际上如果你过去大多是写的Java代码, 那么它们比你所想象的有更少的使用机会。你常常可以让类型声明远离定义,并且让它自己去推断,但是方法的参数必须总是有明确的类型。
从main()方法可以看出,从Scala调用Java代码常常是很容易的,它通过Executors类创建了一些像ServerSocket这样的一些Java对象。
Case类和模式匹配
Scala中比较有趣的一个特性是case类,它像是一种普通的类,但是带有编译器生成的equals、hashCode、toString、支持模式匹配的方法等等。这让我们可以用很少的几行代码创造保存数据的小型类型。例如,我们选择使用一个case类保存HTTP状态行的信息:
case class Status(code: Int, text: String)
在run()方法中,我们能够看到一个模式匹配的表达式。它同Java中的switch类似,但是更加强大。然而,这里我们不去深入了解它真正的强大之处,我们只使用一个 “|(或运算)”模式,还有你能够通过使用 name @的前缀 将一个匹配结果绑定到一个名字(name) .
case method @ ("GET" | "HEAD") => ... case method => respondWithHtml( Status(501, "Not Implemented"), title = "501 Not Implemented", body = 501 Not Implemented: { method } method ) ...
在第二种情况下,当调用respondWithHtml时,我们利用了(默认)已经命名的参数——它们允许我们在调用方法的站点命名参数,以避免不得不去 记忆相同类型参数的顺序,或者仅仅是为了使代码更加纯净。这里我们选择了不去命名状态,因为从Status(...)构造器的调用看来,它的意义已经很明 显了。
有趣的String
另外一个有趣的特性——它在Scala2.10中被加入——它是String差值。你在编写常规的String常量时带上s前缀,它就允许你在 String中嵌入Scala代码,使用${}或者$来使得简单名字区分识别出来。同多行的String结合起来,我们能容易的构造出将被发送的HTTP 头部,不带任何String的串联操作:
val header = s""" |HTTP/1.1 ${status.code} ${status.text} |Server: Scala HTTP Server 1.0 |Date: ${new Date()} |Content-type: ${contentType} |Content-length: ${content.length} """.trim.stripMargin + LineSep + LineSep
(注意:我们可以选择丢弃零参数方法的括号。)
Scala也允许我们实现我们自己的String插值,但是这里用默认实现的已经足够了。
trim方法是通常的Java的String.trim()方法,它用于从头到尾去掉字符串中的空格字符(也包括换行符)。stripMargin方法则 将去掉字符串每一行从开始直到 | (尾部连结)符号的所有东西,并且允许我们在多行字符串上面正常使用缩进。这个方法通过从String到WrappedString的隐式转换被添加到 String类型中,并且如法炮制,我们可以添加我们自己的边缘剥离(margin stripping)逻辑,例如做到让你可以在没有额外的 | 字符的前提下,对每一行执行trim操作。
内置XMl,爱它还是恨它
在respondWithHtml方法中,我们看到另外一个有趣但不那么可爱的Scala特性:内置XML表达式。该方法用一系列的XML节点(scala.xml.NodeSeq), 以XHTML子元素形式,作为输入参数,然后将它们包裹于另一个XML表达式之中,在这些实际的title和body周围增加了HTML/HEAD /BODY元素,再将它转换为字节。我们调用这个方法时,我们可以为body提供XML表达式形式的元素。
def respondWithHtml(status: Status, title: String, body: xml.
NodeSeq) =
...
<HTML>
<HEAD><TITLE>{ title }</TITLE></HEAD>
<BODY>
{ body }
</BODY>
</HTML>
...
避免空指针错误
在toFile和sendFile中,我们使用Scala处理可选值的首选方法,选择类型(请注意:Scala也有空值)。toFile会返回一些(文件)或者如果没找到服务的文件则不反悔文件,然后sendFile会做一个涵盖两种情况的模式匹配。如果我们遗漏了任何一种情况,编译器都将警告我们该情况。
def toFile(file: File, isRetry: Boolean = false): Option[File] = if (file.isDirectory && !isRetry) toFile(new File(file, DefaultFile), true) else if (file.isFile) Some(file) else None
所有东西都是表达式
我们也能利用这一事实——那就是在Scala中几乎所有的构造都是表达式——于是像if-else-if-else这种控制结构实际上就产生一个值。因此我们能够省略掉括弧,直接让方法使用一个表达式。
在sendFile方法中我们可以见到更多的这种东西,在里面我们使用了本地的{...}块,它产生了一个值——这个块的最后一行表达式是它的返回值,如此我们隐藏了块中的临时变量,并且将结果分配到块的最小的一个临时变量中。
val contentType = { val fileExt = file.getName.split('.').lastOption getOrElse "" getContentType(fileExt) }