为什么我们需要如此多的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传进去了。
2 |
void run(String param); |
4 |
public void execute(Action action){ |
一旦我们拥有了一个将功能接口作为参数的方法,我们就可以像下面这样来调用它了:
1 |
execute((String s) -> System.out.println(s)); |
同样的表达式可以用一个方法引用来替换,因为它只是一个使用了相同参数方法调用。
1 |
execute(System.out::println); |
然而,如果在参数在进行着任何变化,我们就不能使用方法引用,而只能使用完整的lambda表达式了:
1 |
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类,但是在我们写下这份报告时,这个类型却没有出现在最新的构建版中了:
2 |
openjdk version "1.8.0-ea" |
3 |
OpenJDK Runtime Environment (build 1.8.0-ea-b75) |
4 |
OpenJDK 64-Bit Server VM (build 25.0-b15, mixed mode) |
如我们所发现的,它已经被Consumer接口替代了,并且被应用于集合资源库中的所有新方法中。例如,Collection接口中像下面这样定义了 forEach 方法:
1 |
public default void forEach(Consumer consumer) |
Consumer 接口的有趣之处在于,它实际上定义了一个抽象方法——accept(T t),还有一个防御型的方法—— Consumer chain(Consumer consumer).。这意味着使用这个接口进行链式调用是可能的。我们还没有在JDK的资源库中找到 chain(...) 方法,因此还不怎么确定它将怎样被应用。
而且,请注意所有的接口都标记上了@FunctionalInterface(http://download.java.net/jdk8/docs/api/java/lang/FunctionalInterface.html)运行时注解。但是除了它在运行时通过注解用javac去确认是否真的是一个功能型接口以外,它里面就不能有更多的抽象方法了。
因此,如果你编译下面这段代码:
3 |
void run(String param); |
4 |
void stop(String param); |
编译器将会告诉你:
1 |
java: Unexpected @FunctionalInterface annotation |
2 |
Action is not a functional interface |
3 |
multiple non-overriding abstract methods found in interface Action |
而下面这段代码将会正常的编译:
3 |
void run(String param); |
4 |
default void stop(String param){} |
防御方法
出现在了 Java 8 中的一个新概念是接口中的默认方法。它意味着,接口不仅可以声明方法的签名,也还可以保持默认的实现。对于这个功能的需求源于对于JDK API中的接口进化需要。
防御方法最显著的应用是在Java的Collection API。如果你使用过Groovy,你可能写过像下面这样的代码:
1 |
[ 1 , 2 , 3 , 4 , 5 , 6 ]. each { println it } |
而如今,我们使用像下面这样的for-each循环来进行迭代操作:
1 |
for (Object item: list) { |
2 |
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方法,并且为它提供了默认的实现。
1 |
public interface Iterable<T> { |
3 |
public default void forEach(Consumer consumer) { |
添加新的默认方法并没有打破源码级别的兼容性,因为接口的实现类并不需要提供它们自己对于这个方法的实现,因此从Java 7切换到Java 8以后,现有的代码还能继续通过编译。如此,在Java8我们能够像下面这样编写循环代码:
1 |
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中是两个不同的命名空间,但是为了方便我们能够引入动态部分中的静态命名空间:
Scala允许在任何地方使用引入(import)语句,你可以从对当前范围可见的拥有成员的任何地方,将成员引入到当前范围之中;因此Scala代码中的这个结构(指import)时间上比Java更加的常用,而Java只允许你将引入语句放在文件的开头,并且只能引入类或者类的静态成员。
与Java比较,还有一个鲜明出众的地方,那就是方法使用def name(argument:Type)的形式定义,并且变量也是如此定义:
1 |
val name: Type = initializer |
或者是:
1 |
var name: Type = initializer |
你应该不会去选择使用可变的变量,所以默认使用val吧——我们的样本代码中并没有任何使用var的定义,因为实际上如果你过去大多是写的Java代码,那么它们比你所想象的有更少的使用机会。你常常可以让类型声明远离定义,并且让它自己去推断,但是方法的参数必须总是有明确的类型。
从main()方法可以看出,从Scala调用Java代码常常是很容易的,它通过Executors类创建了一些像ServerSocket这样的一些Java对象。
Case类和模式匹配
Scala中比较有趣的一个特性是case类,它像是一种普通的类,但是带有编译器生成的equals、hashCode、toString、支持模式匹配的方法等等。这让我们可以用很少的几行代码创造保存数据的小型类型。例如,我们选择使用一个case类保存HTTP状态行的信息:
1 |
case class Status(code: Int, text: String) |
在run()方法中,我们能够看到一个模式匹配的表达式。它同Java中的switch类似,但是更加强大。然而,这里我们不去深入了解它真正的强大之处,我们只使用一个 “|(或运算)”模式,还有你能够通过使用 name @的前缀 将一个匹配结果绑定到一个名字(name).
我们使用的模式是要去匹配一个从HTTP连接输入流读取的HTTP方法名。第一次的模式中我们匹配“GET”或者“HEAD”,第二次则是其它的任何东西:
1 |
case method @ ( "GET" | "HEAD" ) => |
5 |
Status( 501 , "Not Implemented" ), |
6 |
title = "501 Not Implemented" , |
7 |
body = <H2> 501 Not Implemented: { method } method</H2> |
在第二种情况下,当调用respondWithHtml时,我们利用了(默认)已经命名的参数——它们允许我们在调用方法的站点命名参数,以避免不得不去记忆相同类型参数的顺序,或者仅仅是为了使代码更加纯净。这里我们选择了不去命名状态,因为从Status(...)构造器的调用看来,它的意义已经很明显了。
有趣的String
另外一个有趣的特性——它在Scala2.10中被加入——它是String差值。你在编写常规的String常量时带上s前缀,它就允许你在String中嵌入Scala代码,使用${}或者$来使得简单名字区分识别出来。同多行的String结合起来,我们能容易的构造出将被发送的HTTP头部,不带任何String的串联操作:
2 |
|HTTP/ 1.1 ${status.code} ${status.text} |
3 |
|Server: Scala HTTP Server 1.0 |
5 |
|Content-type: ${contentType} |
6 |
|Content-length: ${content.length} |
8 |
"" ".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会做一个涵盖两种情况的模式匹配。如果我们遗漏了任何一种情况,编译器都将警告我们该情况。
1 |
def toFile(file : File, isRetry : Boolean = false ) : Option[File] = |
2 |
if (file.isDirectory && !isRetry) |
3 |
toFile( new File(file, DefaultFile), true ) |
所有东西都是表达式
我们也能利用这一事实——那就是在Scala中几乎所有的构造都是表达式——于是像if-else-if-else这种控制结构实际上就产生一个值。因此我们能够省略掉括弧,直接让方法使用一个表达式。
在sendFile方法中我们可以见到更多的这种东西,在里面我们使用了本地的{...}块,它产生了一个值——这个块的最后一行表达式是它的返回值,如此我们隐藏了块中的临时变量,并且将结果分配到块的最小的一个临时变量中。
2 |
val fileExt = file.getName.split( '.' ).lastOption getOrElse "" |
3 |
getContentType(fileExt) |
尽管这里没有展示出Scala完整的深度,但是我们看出它具有的表现能力,而且这个例子如期提供了关于这个语言是什么的一些观点。关键是拥抱不变性,并且尝试将代码组合建模成表达式。因此相较于我们提供的样本,toFile方法似乎最好从sendFile中提取出来。
Groovy
“Groovy有超过Java将能够提供的甜点,例如它具有轻易地在宿主程序中嵌入并编译,以提供定制业务规则的能力,还有它如何为领域特定语言(Domain-Specific Language)提供优雅,简洁并且可读性好的语法的能力.”
Guillaume Laforge,
Groovy的项目带头人
Groovy入门
Groovy并不像我们在这个报告中涵盖的一些语言那样具有冒险性质,但绝对是你应该感兴趣的一种JVM语言。它已经成为了一种受到开发者信任的成熟选择,这时Java开发商的伤害,以及动态类型这些都不是问题。
无论如何,我都不会是那种争论何时给玩转一种编程语言一次机会的人。
Java变得过分充实
Java开发者可以在Groovy中深入编程并且变得多产。匹配Java的语言有希望或者这看起来将会是未来的趋势,2.0发行版中已经加入了Java7项目的Coin增强.另外Groovy还使得日常使用Java遇到的坎坷变得平滑。安全的导航(?.)以及 Elvis(?:)都算是很棒的例子。
3 |
def streetName = user?.address?.street |
5 |
def displayName = user.name ? user.name : "Anonymous" |
7 |
def displayName = user.name ?: "Anonymous" |
“Groovy是一种多样性的JVM语言.使用一种同Java相近的语法,这种语言是易学的,并且允许你编写从脚本到完整的应用程序代码,包括强大的DSL(领域特定语言)。Groovy很可能是JVM上唯一使得运行时的元编程、编译时的元编程、动态类型以及静态类型容易处理的语言。”
CÉDRIC CHAMPEAU, Groovy中的高级软件工程师
闭包(Closure)
我们预期Groovy将止步于句法功能,然而我们却在文档中又发现“闭包”。为什么称这超越了我们的预期呢,因为Groovy函数值中的一等公民、更高级别的函数以及lambda表达式,这些都得到了支持。
2 |
[ 1 , 2 , 3 , 4 ]. collect (square) |
标准库对于闭包恰到好处的应用使得使用它们成为一种享受,并且也证明了它们的实力。下面是使用闭包的语法糖作为方法后面参数的好例子:
1 |
def list = [ 'a' , 'b' , 'c' , 'd' ] |
3 |
list. collect ( newList ) { |
集合
几乎所有的应用程序都依赖于集合。不幸的是集合大量的戳到了Java的痛处。而如果你怀疑我的这种说法,请尝试做一些有趣的JSON操作。Groovy为集合的定义将原有的语法打包,并且为了强大的可操作能力,着重使用了闭包。
1 |
def names = [ "Ted" , "Fred" , "Jed" , "Ned" ] |
3 |
def shortNames = names.findAll { it.size() <= 3 } |
4 |
println shortNames.size() |
5 |
shortNames.each { println it } |
静态类型
人们常常振奋于动态语言,因为你用很少的代码就能获得更多的功能。这常常很少被理解,而剩下的被带回去维护。因此我们能够看到越来越多的动态语言获得静态类型,而且反之亦然。
Groovy2.0加入了静态类型
静态类型缘何且如何提升了Groovy?
“静态检查使得从Java到Groovy的转型的之路更加的平滑。许多人加入(后续还有更多人加入)Groovy,因为它轻量级的语法,以及所有被移除的样板,但是,举个例子,不想(或者不需要)使用动态特性。他们往往很难理解Groovy编译时不像他们以前那样抛出错误,因为他们实在是不能理解Groovy是一门动态语言。对于他们来说,我们现在有了@TypeChecked。第二个原因是性能表现,由于Groovy仍然支持更老的JDK(现在是1.5),而动态调用支持对它们不可用,因此为了代码的关键性能部分,你可以有静态的编译了的代码。还要注意的是,静态编译对于那些想要免受猴急修补(monkey patching)的框架开发者来说是有趣的(基于能够在运行时改变一个方法的行为这一事实)。”
Cédric Champeau
Groovy的高级程序工程师
Groovy也不例外,静态检查可以通过相关代码中的@Typechecked注解实现.
01 |
import groovy.transform.TypeChecked |
02 |
void someMethod() {} <&br> |
你最喜欢的,用Groovy写成的应用程序/框架/库是什么?还有为什么你对它饱含激情?你能说出超过一种么?
“那很简单,我最感到热情的就是Griffon,一个基于JVM的桌面应用程序开发平台。Groovy被用于作为框架中的原生语言,而其他JVM语言可能也被这样对待。对于web开发,没有Grails我不会知道我会怎样编写web应用,简单来说这东西不用提刷新,还有点意思。Gradle是我工具箱中另外一个宝贝,不管什么时候一有机会我就用它。最后,Spock展示了Groovy编译器的强大,还有AST表达式通过一个简单但是十分强大的测试DSL进行处理。”
Andres Almiray,
Groovy 提交贡献者
Fantom
“Fantom是一个优雅的,强烈关注并发和可移植性的新一代语言。不可变性深深融入了Fantom 的类型系统,并且并发使用了演员(actor)模型。Fantom是着意于轻便性来设计的,并且对于Java VM和JavaScript/HTML都有产品质量级别的实现。Fantom务实于关注静态类型的风格,但是也能轻易的允许进行动态编程。它是一种面向对象语言,但也包含了第一类函数(first class functions),并且许多的标准API都应用了闭包。”
Brian Frank,
Fantom创始人
Fantom入门
Fantom同我们在这个报告中观察的大部分其他语言有一点点不同之处,它的目标是多平台。基于JVM、.Net和JavaScript的编译现在都已经提供支持了,并且鉴于他们已经就位的基础设施,它应该也可能瞄准了其它的目标平台。
但是尽管可移植性和平台成熟度因素是Fantom作者(Brian 和 Andy Frank)考虑的重要问题,它也不是他们要定义这门语言的出处。他们声称Fantom是一个实用的语言,就是为了干实事的。
第一步要做的就是设置环境还有工具。幸运的是,Fantom使得这对于我们来说很容易。Xored搞了一个称作F4的基于Eclipse的IDE,它包含了我们需要让Fantom运行起来的一切。
Pod/脚本(Script)
Fantom可以将文件作为脚本执行,你只需要在文件中放置一个带有main方法的类,并且有一个可执行的扇子(fan)就可以运行它了。
1 |
class HelloWorldishScript |
4 |
static Void main() { echo( "Woah! Is it that easy?" ) } |
然而那不是构建Fantom程序的主要方法。对于大型的项目和提前编译好模块的产品级系统,称作Pod,是使用Fantom的构建工具创建的。
构建是通过一个构建脚本编排的,它本质上是Fantom代码的另一块。下面是HTTP服务器样本Fantom实现的构建脚本:
02 |
class Build : build::BuildPod |
06 |
podName = "FantomHttpProject" |
08 |
srcDirs = [`./`, `fan/`] |
09 |
depends = [ "build 1.0" , "sys 1.0" , "util 1.0" , "concurrent |
这里有几个事项需要注意,例如依赖规范允许我们用比jar包更少烦恼的方式,更加容易的构建更大型的系统。另外,一个Pod不单单只定义部署命名空间,还有类型命名空间,统一和简化了两者。现在你可以发现我们的服务器依赖的Pod:sys、util和concurrent。
Fantom是如何出现支持多个后端(.net,JavaScript)的创意的?
“在过去的生涯中,我们使用Java构建产品,但是有许多想要将解决方案卖到.NET商店中的问题。因此我们设计了Fantom来同时针对两个生态系统。而当我们开始开发现在的产品时,我们将Fantom转入一个新的方向,目标是我们运行在JVM上的后端使用一种语言和代码库,而我们的前端运行在HTML5浏览器上面。多年来,这在目前已经成为了一个非常成功的策略。”
Brian Frank,
Fantom创始人
标准库/Elegance
Fantom的身份不仅仅只是基于JVM平台(或者事实上是任何其它的平台)上的一种语言,而它自身更像就是处在JVM之上的一个平台。平台提供了API,而Fantom确保了API的精彩和优雅。在最基础的层面上它提供了几种文法,像下面这样的:
3 |
Map map := [ 1 : "one" , 2 : "two" ] |
拥有一个周期文法( duration literal)是它的一个微不足道的小细节,但是当你想要设置一个延时操作的时候,你就能感觉到好像已经有人帮你考虑到了这个场景。
IO的API涉及到一些基础的类,如Buf、File、In/OutStreams,它们使用起来令人很愉悦。网络互通功能也提供了,还有JSON的支持,DOM操作和图形库。重要的东西都为你而存在着。Util这个pod也包含了一些很有用的东西。代替一个拥有main方法的类,文件服务器扩展了AbstractMain类,并且能自由的传递参数、设置日志。另一个使用起来令人很愉悦的API是Fantom的并发框架,但我们将只用几分钟来谈论一下它。
互操作(Interop)
所有构建于JVM平台之上的语言都提供了一些与原生Java代码协同工作的能力。这对于利用Java的庞大生态系统起到了关键作用。也就是说,要创建一个比Java好的语言很容易,而创建一个比Java提供的相当得体的互操作更好的语言就难了。那部分归因于对集合的处理(它有点了老旧且平淡,而且有时候使用起来有点儿痛苦)。
Fantom提供了一个Interop类,它有一个toFan方法和一个toJava方法,用于来回转换类型。
2 |
InStream in := Interop.toFan(socket.getInputStream) |
3 |
OutStream out := Interop.toFan(socket.getOutputStream) |
这里你可以发现我们有了原生的Java Socket,它自然的为我们提供了Java的Input和OutputStream。
使用Interop将他们转换成与Fantom地位相同,并且稍后就使用它们。
静态和动态类型?
另一个任何语言都要审查的主题是这个语言是否提供静态/动态类型的支持。
Fantom在这一点处在中庸的位置,并且我们很喜欢这一点。属性域(Field)和方法带有强静态了性的特性。但是对于本地变量,类型就是被推断出来的。这导致了一种直观的混合,方法约束是被拼凑出来的,但是你也并不需要给每一样事物都赋上类型。
自然的,在Fantom中有两种方法调用操作。点(.)调用需要通过编译器检查并且是强类型的,箭头(->)调用操作则不是。这就从一个动态类型的语言中获得了鸭式类型(duck-typing)还有你想要的任何东西。
不可变性(Immutability)&并发(Concurrency)
Fantom提供了一个演员(Actor)框架来处理并发。消息传递和链式异步调用很容纳入代码中。为了创建一个演员(它将由某一种ActorPool支持,而后通过一个线程池获得),你需要扩展一个Actor类,并且重写(奇怪的是你必须明明白白的给override关键词框定类型)receive方法。
请注意避免在线程之间分享状态,Fantom将坚持要你只传递不可变的消息给actor。不可变性在设计时就构建到了语言之中,因此你可以构建所有的属性域都是常量的类。编译器将验证为actor准备的消息事实上是不可变的,否则将抛出一个异常。
比较酷,而且难于发现的一点就是,如果你真心需要一个可变的对象,你可以把它封装到一个Unsafe中(不,这不是那个不安全的概念,而是Fantom中的Unsafe).
2 |
socket := serverSocket.accept |
3 |
a := ServerActor(actorPool) |
稍后你可以把原来的对象再取回来.
1 |
override Obj? receive(Obj? msg) { |
3 |
log.info( "Accepted a socket: $DateTime.now" ) |
4 |
Socket socket := ((Unsafe) msg).val |
----------------------------------------------------
"你应该考虑吧Unsafe作为最后的杀手锏——因为它会侵蚀Fantom中的整个并发模型.如果你需要传递可变的状态 b/w Actor——你应该使用序列化——它是一个内建的特性: http://fantom.org/ doc/docLang/Actors.html#messages"
Andy Frank,
Fantom创始人
这意味着,适当的解决方案在这里就会像这样:
3 |
socket := serverSocket.accept |
5 |
a := ServerActor(actorPool, socket) |
7 |
a.send(“handleRequest”) |
这样我们就能够为接下来为对那个的IO操作存储套接字对象(socket),而我们不需要再去传递任何不可变的消息了.顺便获得的好处是,这样代码看起来更好了.
函数(Functions)& 闭包(Closures)
Fantom是一种面向对象的语言,它像许多其他的现代编程语言一样,将函数做为了头等公民。下面的例子展示了如何创建一个Actor,我们将含蓄指定一个接收函数向其发送几次消息。
pool := ActorPool()
a := Actor(pool) |msg|
{
count := 1 + (Int)Actor.locals.get("count", 0)
Actor.locals["count"] = count
return count
}
100.times { a.send("ignored") }
echo("Count is now " + a.send("ignored").get)
Fantom’s的语法十分的友好,与其他语言没太大出入。我们大概已经发现,它用":="来进行变量的赋值,这对我来说是一种灾难(对这样的小的语法细节,我并不十分喜欢)。然而IDE对此支持得十分友好,每当你犯此错误的时候,它都会提醒你。
一些小东东
在我们研究Fantom的整过过程中,一些促使这门语言更加棒的小东东会令我们惊喜.例如,支持null的类:它们是能够声明一个接受或者不接受null作为参数的方法的一种类型.
通过这种方式,代码不会受到空(null)检查的污染,并且同Java的互操作变得更加的简单了.
值得一提的还有另外一种特性.那就是带有变量插值(Variable interpolation)的多行字符串(Multi-line String):
2 |
"HTTP/ 1.1 $returnCode $status |
3 |
Server: Fantom HTTP Server 1.0 |
5 |
Content-type: ${contentType} |
6 |
Content-length: ${content.size} |
参数可以有默认值,支持混合和声明式编程,还有运算符重载.每一样东西都各得其所.
只有一件东西我们没有看到并感到是一种缺陷,那就是元组(tuples).然而我们只在想要多返回(mutilple return)时才需要那个东西,因此使用一个列表(list)就足够了.
Clojure
“我着手创建一种语言,意在应对我在使用Java和C#编写的一些类型的应用程序——像广播自动化、调度以及选举系统之类那些东西——它们许多都需要解决的并发问题.我发现只用面向对象编程和用那些语言的并发方法,对于处理这些类型的问题并不怎么够好——它们太难了。我是List的拥护者,还有其它的函数式语言,而我想要做的就是解决那些问题,创造一种立足于实际的语言,再也不用拿Java来编程了.”
Rich Hickey,
Clojure创始人在2009年InfoQ访谈中
Clojure 入门
Clojure 第一次出现在2007年 。和一些成熟的语言相比,它相对是新的。Rich Hickey创造了它。它作为Lisp方言在JVM上面。版本1.0出现在2009年。它的名字是一个双关语在C(C#), L (Lisp) and J (Java)。当前Clojure的1.4。Clojure是开源的(发布在Eclipse 公共许可证 v1.0—EPL)
当你开始阅读Clojure的文档的之后,我们决定开始配置环境变量。我们使用Leiningen(它是一个为Clojure准备的构建工具) 。对Clojure来说,Leiningen (或者缩写Lein) 能够执行大部分任务。它能够执行我们期待的来自Maven相关的例如:
- 创造一个项目骨架 (想一想: Maven 原型)
- 操作依赖
- 编译 Clojure代码到JVM classes类
- 运行测试
- 发布构件到一个中央仓库
假如你使用过Maven, 你会感觉使用Lein非常的舒适。事实上,Lein 甚至支持Maven的依赖。然而两者的主要不同点在于 Lein的项目文件由 Clojure写成。然而Maven使用XML(pom.xml)。尽管有可能去开发用Clojure没有其它的,我们不得不承认Lein是一个非常受欢迎的增加物。它真得是某些事情变得更加简单。
开始得到项目骨架,你能够做像下面这样:
$ lein new clojure-http-server
集成开发环境(IDE)的支持
准备好基本的项目结构以后,就可以开始编辑代码了.如果你是Eclipse的长期使用者,你首先要做的一件事情就是找到一个可以处理Clojure 的Eclipse插件.它应该在一个成熟的工作空间中提供语法高亮和代码补全功能.幸运的是Eclipse中有了一个叫做CounterClockWise的Clojure插件.只要安装了这个插件,就可以在Eclipse中新建Clojure项目了.
这样很不错,我们不需要再去管将我们已经使用过的,前面章节提到的Lein,在命令行中创建的Clojure项目然后导入到Eclipse中这种麻烦事了。我们预计CounterClockWise插件提供了像Eclipse的Maven插件有的在pom.xml和EclipseGUI之间两种交互方式,所提供的功能.
仅仅为了好玩,我们也看了看Clooj,它是用Clojure本身开发的一个轻量级的Clojure IDE.下载和运行它都很容易,不过我们发现它同Eclipse相比,就有点黯然失色了.
最后我们用了用最难的方式开发Clojure程序——只在命令行中使用Lein,还有可靠的GVIM作为文本编辑器——主要是想看看Lein是如何详细工作的.
交互式解释器(REPL)
像众多的函数式语言, Clojure提供的命令行shell可以直接执行Clojure语句。这个shell对开发者非常方便,因为它在开发中不仅允许你测试一小端代码,而且允许你运行程序的一部分。
这或许对用Python,Perl开发的码农没什么新鲜的,但对Java开发者来说,这无疑带来更新鲜、更交互的方式来写代码。
函数式编程——另外一种思考方式
Clojure是一种非常类似于Lisp和Scheme的函数式编程语言.函数式范式同那些习惯于Java的面向对象方式并且习惯于其副作用的方式非常不同.
函数式编程推崇:
- 很少或者完全没有副作用
- 如果使用相同的参数区调用,函数就永远返回同一个结果(而不是依赖于对象状态的方法)
- 没有全局变量
- 函数是第一位的对象
- 表达式懒计算(Lazy evaluation of expression)
这些特性并不是Clojure独有的,而是总体上对函数式编程都要求的:
1 |
(defn send-html-response |
3 |
[client-socket status title body] |
4 |
(let [html (str "<HTML><HEAD><TITLE>" |
5 |
title "</TITLE></HEAD><BODY>" body "</BODY></HTML>" )] |
6 |
(send-http-response client-socket status "text/html" |
7 |
(.getBytes html "UTF-8" )) |
同Java的互操作性
Clojure提供了优秀的同Java库的互操作功能.事实上,对于一些基本的类,Clojure并没有提供它自己的抽象,而是超乎你预期的直接使用了Java类来代替.在这个HTTP服务器的示例中,我们从Java中获取了像Reader和Writer这样的类:
1 |
(ns clojure-http-server.core |
2 |
(:require [clojure.string]) |
3 |
(: import (java.net ServerSocket SocketException) |
5 |
(java.io PrintWriter BufferedReader InputStreamReader BufferedOutputStream))) |
创建和调用Java对象是非常直截了当的.而实际上有两种形式(在这个优秀的Clojure介绍中描述到了):
1 |
(def calendar ( new GregorianCalendar 2008 Calendar/APRIL 16 )) ; |
3 |
(def calendar (GregorianCalendar. 2008 Calendar/APRIL 16 )) |
5 |
(. calendar add Calendar/MONTH 2 ) |
6 |
(. calendar get Calendar/MONTH) ; -> 5 |
7 |
(.add calendar Calendar/MONTH 2 ) |
8 |
(.get calendar Calendar/MONTH) ; -> 7 |
下面是一个实际的样例:
2 |
"Create a Java reader from the input stream of the client socket" |
4 |
( new BufferedReader ( new InputStreamReader (.getInputStream client- |
然而,对于一些结构,我们决定要使用到Clojure的方式.原生的Java代码使用StringTokenizer,这样做违背了不可变对象的纯函数式原则,还有无副作用的原则.调用nextToken()方法不仅有副作用(因为它修改了Tonkenize对象)并且使用同一个(或许是不存在的)参数也会有不同的返回结果.
由于这个原因,我们使用Clojure的更加"函数式"的Split函数:
02 |
"Parse the HTTP request and decide what to do" |
04 |
(let [reader (get-reader client-socket) first-line |
05 |
(.readLine reader) tokens (clojure.string/split first-line # "\s+" )] |
06 |
(let [http-method (clojure.string/upper- case |
07 |
(get tokens 0 "unknown" ))] |
08 |
( if (or (= http-method "GET" ) (= http-method "HEAD" )) |
09 |
(let [file-requested-name (get tokens 1 "not-existing" ) |
并发(Concurrency)
Clojure从一开始设计对并发很上心,而不是事后诸葛亮.使用Clojure编写多线程应用程序非常简单,因为所有的函数默认都实现了来自Java的Runnable和Callable接口,自身得以允许其任何方法在一个不同的线程中运行。
Clojure也提供了其它特别为并发而准备的结构,比如原子(atom)和代理(agent),但是我们在这个HTTP服务器示例中并没有使用它们,而是选择熟悉的Java的Thread.
4 |
(.start ( new Thread (fn [] (respond-to-client client-socket))))) |
有关方法使用顺序的问题
我们认识到的一件事情是在源代码文件中,方法的顺序是严格的。函数必须在它们第一次被使用之前被定义。另外一种选择是,你可以在一个函数被实际定义之前利用一种特殊的声明格式,来使用函数。这让我们想起了C/C++的运作方式,它们使用头文件和函数声明。
Ceylon
"Ceylon由Java启发产生。我们试图去创造一个更强大的语言。我们时刻牢记不能做的更加糟糕比起Java - 那就是说,我们不想打破某些Java做的好的地方或者我们不想失去某些特征。那就是在团队环境中,Java能够编写大的稳定的项目。"
Gavin King,
Ceylon 的语言的创造者
Ceylon 入门
作为一个Java开发者, 你将会希望能够非常快地采用Ceylon。在项目的主页上,开始编码基本上相当于走Ceylon新的旅程 - 从那里你将会得到大部分关于语言的信息。
对于Ceylon,我们首先要关注的是它精心准备的“基础设施(infrastructural)”部分。在这里基础设施的意思是模块。Ceylon运行时是基于JBoss的模块(Module)的,并且Ceylon用模块代表一切事物。即使是你新打造的项目实际上也是一个Ceylon模块,它是通过项目文件夹里面的一个module.ceylon文件来声明的:
1 |
module com.zt '1.0.0' { |
2 |
import ceylon.interop.java '0.4.1' ; |
这里的意思是这个模块叫做com.zt,它的版本号是1.0.0。关于模块声明的格式需要注意:模块名不需要在引号中,而模块的版本号需要在引号中,比如'1.0.0'。这有点怪吧,因为版本号是一个数字而包不是的。为什么不省掉版本号的引号呢?可能是因为版本号会包含非数字的字符吧,像'1.0.0-SNAPSHOT’。但是完全省掉引号,Ceylon也可能理解。
Ceylon出色的践行了模块化,并且将运行时构建于Jboss模组。这些为什么如此重要?
首先,没有模块化,你就不能在不去破坏一些东西的前提下解决问题。
其次,没有模块化,你的程序就不能跨平台运行。Java庞大的SDK意味着我不能确定在一个JavaScript上面能跑Java程序。Ceylon"小巧"的语言模块能在一个web浏览器上面工作得像在服务器上面一样好。
第三,没有模块化,你就不能构建在模块产品上作业的工具,也不能用模块资源库代替放置在你硬盘的单个的文件。
第四,没有语言级别的模块化,你就会滋长像Maven和OSGI一样的可怕的过度工程的技术。你也不得不在臃肿庞大的像JavaSE和JavaEE(JavaEE6之前)这样的平台上工作。
Gavin King
Ceylon创始人
其次,run.ceylon是项目的主文件,而我们可以实现run()方法来启动我们的应用:
2 |
print( "Hello from Ceylon!" ); |
Ceylon中的方法(或者函数)可以是单独的,而不属于任何一个类。属性(attribute)也同样适用这一特性。实际上,发现它编译成了什么是相当有趣的一件事。每一个单独的属性(或域)和方法都被编译成了一个专用的类。那意味着下面的这段代码被编译成了两个类,port_.class和run_.class——它们也同样包含了公共静态返回值为空的main (public static void main)方法.
02 |
ServerSocket server = ServerSocket(port); |
03 |
print( "Listening for connections on port " port "..." ); |
05 |
Socket socket = server.accept(); |
06 |
print( "New client connection accepted!" ); |
07 |
HTTPServer httpServer = HTTPServer(socket); |
09 |
Thread threadRunner = Thread(httpServer); |
在这个过程中我们了解到,Ceylon工具编译和打包了所有东西到Ceylon ARchive(.car)中,并且在那之后对自身做了清理,所以你不会在编译之后找到class文件——我们只有解压这个压缩了的存档才能访问class文件。
工具的一个很棒的部分是,当一个新的依赖被加入到module.ceylon文件中时,依赖会自动从CeylonHerd网站下载,并且由IDE处理识别。那是这个语言的基础设施的另外一个长处。
同Java的互操作性
同Java的互操作性对我来说是一个棘手的部分。首先,你必须将依赖添加到module.ceylon中,使得程序能够使用Java类:
当你在Ceylon中使用Java类时,你可能会直观的期望着可以像在Java程序中一样正常的使用Java类的实例中的所有方法。但是在Ceylon中不是这样的。下面是一个实例:
1 |
value printWriter = PrintWriter(socket.outputStream); |
你希望能在socket上面使用getOutputStream()方法,但是Ceylon将它解释成了一个域的获取器(getter),并且用一个属性访问修饰(大概是?)来取代它的语义。尽管当你检查它的来源时,getOutputStream()并不是一个获取器。
类型转换是我们需要面对的第二个挑战。Ceylon类型和Java类型之间有一种特殊的映射。
因此fileData一定是一个字节数组类型,而根据类型映射规则,它应该在Ceylon中被声明为一个Array:
1 |
Array fileData = arrayOfSize { |
2 |
size = fileLength; element = 0 ; |
噢,我的...(天),不好了:
1 |
Exception in thread "Thread-0" java.lang.ClassCastException: |
2 |
[J cannot be cast to [B |
3 |
at com.zt.HTTPServer.run(classes.ceylon: 59 ) |
4 |
at java.lang.Thread.run(Thread.java: 722 ) |
显然,为了让类型转换能正常的工作,我们不得不 (在module.ceylon中)导入特殊的Java互操作模块:
1 |
import ceylon.interop.java '0.4.1' |
然后才有可能使用由互操作模块提供的帮助方法。下面就是类型转换所需要的代码:
1 |
Array<Integer> fileData = createByteArray(fileLength); |
2 |
value fileIn = FileInputStream(file); |
"事实上这是一个真正令人痛苦的角落。我们处理Java原生类型的一般性策略在处理器容器类型时是不健全的,这通常是很好的,因为Java的泛型并不是基于Java原生类型的抽象:你不能拥有一个List。但是当Java有了数组(array)时,它们是特殊类型的容器类型,是基于原生类型的抽象,我们因此需要用特殊的方法处理这种特殊的情况。这个问题将会在未来版本的编译器中被修复。"
Gavin King
Ceylon创始人
基本上,毕竟HTTP服务器实现的所有问题都已经在运行着了,但是在运行时会出现惊喜:null检查。
现在应用程序启动了,但是在客户端提交了一个GET请求以后,它失败了,打印如下的调用栈:
1 |
Exception in thread "Thread-1" java.lang.NullPointerException |
3 |
at com.redhat.ceylon.compiler.java.Util.checkNull(Util.java: 478 ) |
4 |
at com.zt.HTTPServer.run(classes.ceylon: 19 ) |
这个令人惊奇的部分位于classes.ceylon第19行,这里有这样一行代码:
1 |
String input = bufferedReader.readLine(); |
这里是一些如何使用javap帮助来阅读反编译代码的知识。字节码透露了Ceylon编译器在Java类的方法返回之后插入了null检查。因此要解决这个问题,我们不得不在声明中使用?后缀:
1 |
String? input = bufferedReader.readLine(); |
但是随后就会有这个值是如何被使用的问题。例如,如果它是作为StringTokenizer的一个参数使用,那么初始化这个类应该也会失败。因此就有了下面这种蹩脚的解决办法:
肯定应该有处理这种情况的更好方式,而不是像眼前这样就处理了问题。
"如果有你确实不想在null的情况下再做一些什么事情的场景,你可以就写“assert(exists input);",那就会将范围缩小到非空的类型。”
Gavin King
Kotlin
“我们认为Kotlin的定位是一种现代化工业语言:它专注于代码重用和可读性的弹性抽象,以及面向早期错误侦测,和明确捕获维护与清理的意图,这些问题的静态类型安全性。Kotlin最重要的使用场景之一是对于一个庞大的Java代码库,其开发者需要一个更棒的语言:你能够将Java和Kotlin自由混合,迁移可以是渐进式的,不需要一下子对整个代码库进行改变。”
Andrey Breslav
Kotlin创始人
考察Kotin时,作为JetBrains创造的产品,我们首先要面对的问题就是其他IDE对它的支持。由于我们大多数人都是Eclipse的用户,切换到IntelliJ IDEA环境总是困难的,但是如果你想使用Kotlin环境进行编码工作,这总是免不了的一个步骤。安装Kotlin插件式相当容易的,Kotlin的artifact也是,但令人难堪的就是支持没有推广到其它的IDE。也许我们应该通过某种恰当的方式向JetBrains团队就这个问题发出一些暗示才行?:)
优雅的编码
使用Kotin进行编码确实会生产出一些非常优雅的代码。它消除的进行null检查的必要,使用主构造器(primary constructors),智能的转换(smart case),范围表达式...这个清单还没完呢。那就让我们先来看个例子吧。
以我们Java的背景来看,我们不喜欢使用when结构铸造(casting)的is组合。在Java中,将它们看作是铸造的(A)对象的实例,各自进行转换。is的使用也将会引用一个铸造(cast),如果你率先直接使用这个对象的话。比如:if(stream is Reader)stream.close()。在这个例子中,close方法在Reader接口上被调用了。这应该也可以像这样子表述:if(stream is Reader)(stream as Reader).close(),但是不需要额外的代码了。这种使用when的组合允许你对变量进行切换,但当你能够获得一种更加丰富的参与时,就不仅仅只使用它的值了。你可以考虑像下面这样:
2 |
is Reader -> stream.close() |
3 |
is Writer -> stream.close() |
4 |
is InputStream -> stream.close() |
5 |
is OutputStream -> stream.close() |
6 |
is Socket -> stream.close() |
7 |
else -> System.err.println( "Unable to close object: " + stream) |
如果你能想象你将会怎样在Java中实现这个示例,你就会觉得上面的代码有多干净优雅了。有趣的是C#也有类似的is和as的使用方式,同样也实现了可为空(nullable)类型。
利用参数命名(parameter naming)的方法调用也是默认的。这种问题也能在你需要调用一个拥有五个布尔值参数的方法,确保你传入的true和false顺序正确时,让你感到头痛。参数命名通过在方法调用上带上参数名称来绕过参数的混淆。这样很棒。这在过去已经被其他的语言和脚本框架再一次做到了,它们允许当用户乐于接受一些默认参数值时,方法调用可以省略掉某些参数。让我们来看一个例子:
1 |
private fun print(out : PrintWriter, |
3 |
contentType: String = “text/html”, |
4 |
contentLength : Long = - 1 .toLong(), |
5 |
title : String, body : () -> Unit) |
这里我们有了一个带有若干个String参数的方法,还有一个作为输入的函数。它使用下面的方式来调用。请注意contentType和contentLength参数都被省略了,意味着声明中的默认值将被使用。
2 |
pre = "HTTP/1.0 404 Not Found" , |
3 |
title = "File Not Found" , |
1 |
404 File Not Found: " + |
那么这有什么意义呢?这将省掉很多方法重载!虽然言重了一点,但是一想到Java在过去超过20多年时间里没有像这样的好东西出世,就足以让这个功能令人惊奇了。有时候就有一点点感觉你是在暗无天日的编写着代码。加点油吧,Java,你得奋起直追才行!
Kotlin将帮助你编写安全的代码,除非你不想这样做
首先,你可以对NPE(空指针异常)说再见了!Kotlin使用“可为空(nullable)类型”和“不可为空(non-nullable)类型来将可能会为空的变量和永远不会为空变量进行区分。考虑下面的代码:
为了允许为空,变量必须被声明为nullable,在这个场景中,就写上 String?:
现在,如果你调用一个使用了变量a的方法,它会担保不会出现NPE,因此你可以安全的说:
但是如果你使用b调用同一个方法,那就不怎么安全了,编译器会报告说出了一个错误:
通过知晓哪些变量可为空,Kotlin编译器负责当你解绑一个可为空的引用时,你可以使用下面的方法其中之一来做到:
Kotlin中的安全调用同Groovy中那些功能非常相似,包括记号(notation)语法。通过像下面这个例子一样使用‘.?’解绑一个可为空类型,告诉编译器调用对象b上面的length方法,而一旦b为空,就不做这一步调用length的操作。
你考虑的那些”什么?避免null?那样做有啥乐趣?”,!!操作符允许抛出潜在的NPE,如果你希望这样的话。
你也可以使用?标记来避免掉假如转换(cast)是不可能时,异常被抛出的情况。这称作一个安全转换。
函数
函数可以在(类)的内部创建(成员函数)或者在类之外。函数可以包含其他函数(局部函数),你可以使用函数去扩展已经存在的类,像下面这样:
fun Int.abs() : Int = if (this >= 0) this else -this
该例子扩展了Int类,返回绝对值。
函数功能非常强大,在JVM上用的很好(在Java里仍然没有,直到lambda表达式在Java8里出现)。Kotlin 也允许使用更高的命令函数,也就意味着你可以通过将一个函数作为参数以让方法进行调用(函数文本)。
文档/帮助
我们发现Kotlin的文档都在同一个地方。所有的Google的搜索都指向了社区网站。这有点儿糟糕,因为有时候去一些地方找找其他的示例和资源来看看也是不错的,而它仍然是相对年轻的。如果有其他人跟进,从而能在Github上面看到更多代码的话,就好了。
Xtend
“Xtend是一种静态类型编程语言,它可以被翻译成可读的Java源代码.它支持现存的Java风格,而且被设计得比Java本身同现存的Java API能工作得更加好。Xtend是一个构建实用抽象的强大和灵活的工具,并且配备了先进的EclipseIDE集成.它是Eclipse的一个开源项目.”
Sven Efftinge
Xtend创始人
Xtend自诩为现代化的语言。它的速度比Groovy更快,比起Scala更简单, 吸取了Java所有的好处。
现在有很好Xtend的Eclipse IDE开发工具,它是由Xtext语言写成的。我们不需要去问为什么,因为Xtend和Xtent都是Eclipse项目。其它的IDE fanboys(Anton)不会紧密的结合和选择其他的IDE,类似的Kotlin 宁可选择 IntelliJ IDEA。
各就各位,预备,写代码!(On your marks,Get set,code!)
我们直接从Eclipse获得了一些代码,发现它其实(对于Java开发者来说)相当的熟悉.Xtend的语法仍然同Java是一样的,这可以同时被看做是好事和坏事,取决于你对于Java语法的个人印象:)它更简单了,一般的Java装备(baggage)都去掉了(最新的lang也是),留下了更好的可读性,而仅仅使用其结构(structure)和关键词,留给我们一个可读性更好的,更加干净的实现.让我们来看看一些代码:
3 |
def static void main(String[] args) |
5 |
println( 'Gangnam, Style' ) |
请注意我们不喜欢用"hello world"这个例子,因为你很少从它们这样的例子里获得更多的东西,因此这里使用我们的"江南(Gangham)Style"示例:)到目前为止我们看到同Java相比极少有改变,但我们需要注意到的是IDE中有一个xtend-gen源代码文件夹,它里面有从Xtend代码翻译的Java代码:
2 |
import org.eclipse.xtext.xbase.lib.InputOutput; |
3 |
@SuppressWarnings ( "all" ) |
5 |
public static void main( final String[] args) { |
6 |
InputOutput.println( "Gangnam, Style" ); |
有趣的是它通过自己实现的InputOutput类来进行输出。
回顾Xtend HTTP server 样例, 我们写了137行代码, 被翻译成309行Java代码。被创建的Java代码不太可读并且也不是很顺利的编辑, 所以我不确信为什么这被显示。
有趣的是, 在我们开始编译Xtend代码的时候,Java代码并没有出现这种情况 :)
为什么你决定要将你的Xtend代码翻译成Java代码,而不是直接翻译成字节码?
拥有快速和优化了的Java编译器,Xtend代码一般能运行得和详细的Java代码一样快.
到目前为止,最大的好处是透明性.Xtend代码翻译成Java源代码能让你了解Xtend编译器实际在干的事情.Java是普遍被了解和受到信任的,翻译成它给了人们所需要的信心.这在当你是一个新手,而且想要学习代码的一个特定片段是如何被翻译的,还有编译器如何在覆盖之下工作的,这些时候是很有帮助的.你能够一直有Java代码作参考.基于Eclipse的Xtend IDE甚至提供了所谓的"生成代码视图(Generated Code View)",它可以对应显示Xtend代码和生成的Java代码。你可以通过选择Xtend代码的一个非常细粒度的部分发现Java代码是如何生成的.
在调试期间,你也能够在Xtend调试和Java源代码调试之间切换.Xtend生成了我所谓的"声明化的(statementisized)"Java代码.函数式编程典型的长长的链式表达式被翻译成了带有合成(synthetic)变量的多行语句.因而你能够逐一浏览单独的表达式,并且查看其中间结果.
Sven Efftinge
Xtend创始人
你能用 Xtend 做哪些 Java 做不到的事情?
这是非常有趣的地方,因为Xtend提供了许多Java缺乏的特性。例如,它提供了扩展的方法,导入的扩展,lambdas,数据集对象,隐式类型和更多。这些很棒! 但是你可以在这个报告中所描述的许多其他语言中列出几乎所有这些。是什么让这门语言有所不同呢?在我们看来,它不是试图成为不同,而是试图成为现在的Java和未来的Java之间差距的桥梁。它是打了激素的Java。
代码段
已经说得够多了!需要更多的图片和代码!让我们深入语言的一些灵活部分,开始先看看扩展方法吧.
这里我们创建了一个新的方法,称作sendHtmlResponse,它有四个参数,然而这里有一个灵活的语法招数,允许第一个参数是我们尝试去扩展的对象.
01 |
def sendHtmlResponse(Socket socket, String status, String title, String body){ |
04 |
<HEAD><TITLE>«title»</TITLE></HEAD> |
09 |
socket.sendHttpResponse(status, "text/html" , html.toString. getBytes( "UTF-8" )) |
这意味着我们实际上可以在Socket对象上面调用sendHtmlResponse方法.
1 |
socket.sendHtmlResponse( |
2 |
"HTTP/1.0 404 Not Found" , |
4 |
"<H2>404 File Not Found: " + file.getPath() + "" ) |
当然在我们的xtend-gen路径中,我们实际上生成的代码像下面这样:
1 |
this .sendHtmlResponse(socket, "HTTP/1.0 404 Not Found" , "File Not Found" , _plus_3); |
而这破坏了错觉(illusion),能够去扩展类真的是非常棒的,这实际上就是Xtend名字的由来之处.
在我们第一个代码块中,我们使用了下面这种结构:
2 |
<HEAD><TITLE>«title»</TITLE></HEAD> |
这被称作模板表达式,被用在了许多其他的语言上.它允许我们很容易的创建HTML,而不用去磕磕绊绊的使用out.print()语句,从而允许我们更好的考到我们创建的文档是什么样的.你会注意到我们可以在语句中标记了变量,并且你也可以在模板中使用条件控制结构.
我们也会想要其他语言中默认使用的命名参数,我们坚持认为不管如何,它都是所有语言的一部分!它(Xtend)确实使用了隐式类型,尽管这样还不错.语言本身仍然是静态类型的,但是变量的类型是在编译的时候由表达式将会赋予的值来决定的.例如:
2 |
val DEFAULT_FILE = "index.html" |
在这个例子中,开始的两个变量都明确是String,而第三个则在一开始不能确认,因此我们需要将它的类型声明为Socket。这就使得代码同下面的Java代码比起来更加的清楚干净:
1 |
private final String WEB_ROOT = "." ; |
2 |
private final String DEFAULT_FILE = "index.html" ;/br> private final Socket socket; |
更多些的语法差异带来了更多的工作要做,像"我们的类方法去哪儿啦!"噢,你需要使用一种不同的标记来做到——File::分割符。我不太确定为什么要这样做,但这不是一个大问题,因此不会对我们造成太多困扰.
这里有趣的事情是,IDE可能会针对一个问题抱怨许许多多的事情出来.这可能会令你相当泄气,因为你不得不按照Xtend编译器想要依循的顺序去修复这些东西,而不是你想的顺序.这方面Java工具仍然有很丰富的致胜经验.
同Java的互操作性
许多语言有时候很难实现同Java的互操作,Xtend真的不想有它们那种问题,它们分享了一个近乎相同的类型系统,提供一个同这种语言简单的往来映射.你能够从Xtend中调用Java,而且毫无疑问,反之亦然。这里提到这些就够了。
综上所述
tl:dr(too long,didn't read)太长了,不好阅读
这个报告已经很棒了——用一种真正极客(geeky)的方式,教育、挑战和娱乐着!对我们来说学习这些新的语言,并且尝试去正视这些语言之间的区别,是很有趣的.他们同Java有明显相当大的不同,不可同日而语.更加新的JVM语言中闭包都明显是重要的,而围绕许许多多的关于如何处理null的问题,它们的观点存在着一种有趣的分裂。下面是我们所认为的你应该对每一种语言要了解的事情:
Java仍然是活蹦乱跳的!我们可以预见lambda和伴随的特性(防御方法,升级的集合库)将很快在Java中产生重大影响.语法相当不错而且一旦开发者意识到这些特性能够带给他们生产力的价值,我们将在未来看到许许多多利用这些优势的代码.
我们看到,Scala能够非常富有表现力,并且相比Java,它可以允许我们离开许多样板语法。这是一个真正的优势,虽然我们的HTTP服务器的例子可能表达不出来,但这也许能给你一些想法来了解这个语言是什么样子。关键的一点,就是拥抱不变性和尝试象可被组成的表达式一样塑造代码。
作为Java开发人员,Groovy很容易上手,并是Web应用程序,DSL,模板和脚本引擎的一个很棒的选择。它处理了在Java中常见的烦恼,包装了特性的现代集合,并有一个很棒的标准库。Groovy会继续被广泛采用, 是JVM的动态语言的一个很好选择。我们只是怕它在特征集持续增长的同时失去焦点。
Fantom是一个成熟和稳定的编程语言有着非常优美的API。 我们绝没有说我们已经完美了, 如果可以使Fantom更好 ,你可以改善它: 我们 Github 仓库对comments 和 pull requests开放。如果你只是观其表面,你只能通过语法来判断编程语言。 Fantom 有着类Java的语法,没有分号并且会抛出了一些自身的关键字。否则, 这肯定是一个来源于Java的优秀语法。
在JVM上面,Clojure 提供了强大函数式语言平台上。如果开发有Lisp/Scheme经验,那是非常爽的一种方式。然而,Java开发者不仅仅是面对新的语法而且新的范式。
尽管它会被滥用类似像JAVA那样,那不是最优的。在将来,Clojure 的强大之处在于并发方面会提供很多便利。越来越多的程序员都会感兴趣代码的并行之处。
Ceylon的语法和关键字对Java而言,非常的不一样。然而,它不会困惑我们多少。 在一些场景下面, Ceylon和Java相关性很强。对实例化而言, 'satisfies' 在Ceylon和 'implements' 在Java想对应。
总的来说,有一个相当有趣的经历。我们对Ceylon的印象是聚集OOP和基础设施(模型系统)风格,而不是简短的表达式。 不要尝试百万变成一行。Ceylon创造者担心Java语言的冗长,重点在于生态系统和语言设计。
总的来说,Kotlin 是一门友好的语言。尽管Kotlin没有特别的创造性,它的优势在于吸收别语言最精华的部分。
该语言非常友好去使用,具有函数,参数名,默认值,扩展类等特点。 它也是一个漂亮的与语言! 关键字的比例:在Kotlin中,代码在构造的时候例如,'when','is'和'as'更加好。
每个2-3三月,发布一个正式版本,Scala开发者可能会转向去使用Kotlin这门语言。尽管我们认为Scala用户不会太有兴趣。
Xtent提供了Java只承诺在未来实现但目前缺乏的简洁明了的功能。问题是,我们会建议让别人在Java或其他的JVM语言之上使用它吗?
然而,对Java用户而言这取决于这些好处能持续多久,一旦Xtend中附加的的这些这些不错的功能在Java中像lambda和扩展的方法一样,被实现了的话,我们不相信有足够的理由转移到Xtend上来。我们能想象使用其他诸如Scala的JVM语言的开发者将Xtend看做Java n + 1或着带有一种插件的Java,而不是一种打破了所有界限的语言,因此我们不会理解为什么Scala的用户可能会破天荒的去使用Xtend。