新发布的Groovy2.0为这门语言带来了关键的静态特性:静态类型检查和静态编译;采用了JDK 7相关的改进:Project Coin语法增强和新支持的“invoke dynamic” JVM指令;同时,提高了模块化。我们将在这篇文章中了解这些新特性的详情。
Groovy天生而且永远都是动态语言。但Groovy也常被当作"Java脚本语言",或是“更好的Java”(即限制更少且功能更强的Java)。实际上,许多Java开发者将Groovy作为一种扩展语言使用或嵌入到自己的Java应用中,如编写表现力更强的业务规则、为不同客户进一步定制应用等。对于这种面向Java的使用场景,开发者并不需要这门语言提供的所有动态能力,他们通常期望能从Groovy编译器得到跟javac编译器一样的反馈。特别是,他们希望得到编译错误(而非运行时错误),如变量或方法名的拼写错误、错误的类型赋值等。这就是Groovy 2支持静态类型检查的原因。
静态类型检查器建立在Groovy已有、强大的AST(抽象语法树)之上,不熟悉它们的开发者可以将其视为一种利用注解触发的可选编译器插件。作为可选特性,不需要它时,Groovy不会强制你使用。要触发静态类型检查,只需在方法或类上使用@TypeChecked
注解就可以在你期望的粒度级别打开检查。让我们首先看一个示例:
import groovy.transform.TypeChecked
void someMethod() {}
@TypeChecked
void test() {
// 编译错误:
// 找不到匹配的sommeeMethod()
sommeeMethod()
def name = "Marion"
// 编译错误:
// 没有声明变量naaammme
println naaammme
}
我们用@TypeChecked
注解了test()
方法,它告诉Groovy编译器在编译时对指定的方法进行静态类型检查。我们试图调用带有明显拼写错误的someMethod()
,并打印另一个拼错的name变量,编译器会分别抛出2个编译错误,因为找不到对应的方法和变量声明。
静态类型检查器还会验证返回类型和赋值是否一致:
import groovy.transform.TypeChecked
@TypeChecked
Date test() {
// 编译错误:
// 不能把Date赋给
// int类型的变量
int object = new Date()
String[] letters = ['a', 'b', 'c']
// 编译错误:
// 不能把String类型的值赋给
// Date类型的变量
Date aDateVariable = letters[0]
// 编译错误:
// 无法在返回Date类型的方法中
// 返回String类型的值
return "today"
}
在这个示例中,编译器会抱怨这样的事实:你没法把Date
赋给int
变量,也没法返回String
来取代方法签名中指定的Date
。正中间脚本引起的编译错误也很有趣,因为它不仅抱怨了错误的赋值,而且还因为它展示了动态类型推断的能力,这当然是由于类型检查器知道letters[0]
是String
类型,因为我们正在处理一个String
数组。
既然谈到了类型推断,那我们就看看它的一些其他表现形式。我们曾说过类型检查器会跟踪返回类型和值:
import groovy.transform.TypeChecked
@TypeChecked
int method() {
if (true) {
// 编译错误:
// 无法在返回int类型的方法中
// 返回String类型的值
'String'
} else {
42
}
}
若方法返回原始类型的int
值,类型检查器还能够检查出不同结构的返回值,如if/else
分支、try/catch
块或switch/case
块。在该示例中,if/else
块的一个分支试图返回一个String
值而非原始类型的int
,这时编译器就开始抱怨了。
但静态类型检查器不会对Groovy支持的某些自动类型转换进行抱怨。例如,对于返回String、boolean
或Class
的方法签名,Groovy会自动将返回值转换到这些类型:
import groovy.transform.TypeChecked
@TypeChecked
boolean booleanMethod() {
"non empty strings are evaluated to true"
}
assert booleanMethod() == true
@TypeChecked
String stringMethod() {
// 调用toString()将StringBuilder转换成String
new StringBuilder() << "non empty string"
}
assert stringMethod() instanceof String
@TypeChecked
Class classMethod() {
// 会返回java.util.List类
"java.util.List"
}
assert classMethod() == List
静态类型检查器的智能足以完成类型推断:
import groovy.transform.TypeChecked
@TypeChecked
void method() {
def name = " Guillaume "
// 判断出是String类型(就算它是在GString中)
println "NAME = ${name.toUpperCase()}"
// 支持Groovy GDK方法
// (也支持GDK操作符重载)
println name.trim()
int[] numbers = [1, 2, 3]
// 元素n是int
for (int n in numbers) {
println
}
}
尽管name
变量是用def
定义的,但类型检查器还是知道它是String
类型。接下来,当这个变量被插入用在string中时,它知道name变量能调用String的toUpperCase()
方法,或者之后的trim()
方法,该方法是由Groovy Development Kit添加用来装饰String
类的。最后,当循环原始的int
数组时,它还知道数组中的元素明显就是int
。
记住一点很重要:使用静态类型检查工具会限制你能在Groovy中使用的特性。大多数运行时动态特性是不允许的,因为它们没法在编译时被静态类型检查。因此,通过类型的元类(metaclass)在运行时添加一个新方法是不允许的。但是,当你需要使用一些特殊的动态特性时,比如Groovy的构建器(builder),只要愿意,你可以选择不使用静态类型检查。
@TypeChecked
注解可用于类或方法级别。因此,要是想对整个类进行类型检查,就把它用在类上,若只想对某些方法进行类型检查,可以把它用在那些方法上。此外,若想对所有内容进行类型检查,但排除某个特殊方法,你可以对被排除方法使用@TypeChecked(TypeCheckingMode.SKIP)
- 或简化版本@TypeChecked(SKIP)
,前提是你静态导入了相关枚举。以下脚本说明了这种情况,greeting()
方法需要类型检查,而generateMarkup()
方法不需要:
import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder
// 这个方法和它的代码要进行类型检查
@TypeChecked
String greeting(String name) {
generateMarkup(name.toUpperCase())
}
// 这个方法不需要类型检查
// 并且你可以使用像markup builder这样的动态特性
String generateMarkup(String name) {
def sw =new StringWriter()
new MarkupBuilder(sw).html {
body {
div name
}
}
sw.toString()
}
assert greeting("Cédric").contains("<div>CéDRIC</div>")
当前的Java发行版不支持通用的类型推断;因此今天我们发现很多地方的代码往往相当冗长并且结构混乱。这掩盖了代码的意图,而且没有强大的IDE支持也很难写代码。这是instanceof
检查的应用场景:你经常会在if
条件中使用instanceof检查值的类,并且在if
块之后,你必须使用对象转型(cast)才能使用这个对象值的方法。用一般的Groovy代码,结合新的静态类型检查模式,你可以彻底摆脱那些对象转型。
import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder
@TypeChecked
String test(Object val) {
if (val instanceof String) {
// 不同于Java的写法:
// return ((String)val).toUpperCase()
val.toUpperCase()
} else if (val instanceof Number) {
// 不同于Java的写法:
// return ((Number)val).intValue().multiply(2)
val.intValue() * 2
}
}
assert test('abc') == 'ABC'
assert test(123) == '246'
在上面的示例中,静态类型检查器知道参数val在if
块中是String
类型,在else if块中是 Number
,无需任何转换。
静态类型检查器在类型推断方面走得更远,从某种意义上讲它对你的对象类型了解更精细。考虑下面的代码:
import groovy.transform.TypeChecked
// 推断返回类型:
// 一个可比较和可序列化的数字列表
@TypeChecked test() {
// 一个整型和一个BigDecimal
return [1234, 3.14]
}
在这个示例中,凭直觉,我们返回了一组数字:一个Integer
和一个BigDecimal
。但是静态类型检查器计算了我们所说的“最低上限(lowest upper bound)”,它实际上是一个数字列表,而且是可序列化和可比较的。用标准Java类型符号不可能表示该类型,但如果我们有一些类似与操作(&)的交集操作符,它看起来就像List<Number & Serializable & Comparable>。
尽管其实不应该将这种做法视为好实践,但有时开发者会使用相同的无类型变量来存储不同类型的值。看看方法体:
import groovy.transform.TypeChecked
@TypeChecked test() {
def var = 123 // 推断出的类型是int
var = "123" // 用一个String给var赋值
println var.toInteger() // 没问题,不需要转型
var = 123
println var.toUpperCase() // 出错了,var是int型!
}
var
变量一开始被初始化为int
。然后,被赋给一个String
。"流式转型(flow typing)"算法根据赋值流程知道变量现在持有一个String
,所以静态类型检查器会乐于接受由Goovy添加到String
上的toInteger()
方法。接下来,一个数字被放回到var变量中,但是紧接着调用toUpperCase()
时,类型检查器将抛出一个编译错误,因为Integer
上没有toUpperCase()
方法。
对于被共享给对其感兴趣的闭包中的变量,流式转型算法有些特殊的情况。当局部变量被定义该变量的方法中的闭包引用时,会发生什么?看看这个示例:
import groovy.transform.TypeChecked
@TypeChecked test() {
def var = "abc"
def cl = {
if (new Random().nextBoolean()) var = new Date()
}
cl()
var.toUpperCase() // 编译错误!
}
局部变量var
被赋值为String
,但接着,若某个随机值为真,var
可能会被赋值为Date
。一般情况下,只有在运行时我们才确切知道闭包的if语句中的条件为真还是假。因此,编译器不可能在编译时知道var
现在是String
还是Date
。这就是编译器对于toUpperCase()
调用抱怨的原因,因为它无法推断变量包含的是String
。这个例子虽略显做作,但是下面有一些有趣的例子:
import groovy.transform.TypeChecked
class A { void foo() {} }
class B extends A { void bar() {} }
@TypeChecked test() {
def var = new A()
def cl = { var = new B() }
cl()
// var起码是个A的实例
// 所以我们允许调用foo()方法
var.foo()
}
在上面的test()
方法中,var
被赋予A
的一个实例,然后在闭包中被赋予B
的一个实例,因此我们至少可推断出var类型A
。
所有这些添加到Groovy编译器中的检查都是在编译时完成的,但是生成的字节码像往常一样仍是相同的动态码 - 在行为上根本没变。
由于编译器现在知道你程序中类型方面的很多事情,它向许多有趣的能力敞开了大门:静态编译那些被类型检查的代码怎样?除了其他优势,一个明显优势是生成的字节码将更接近于由javac编译器自己生成的字节码,让静态编译过的Groovy代码跟纯Java代码一样快。在下一节,我们将了解更多关于Groovy静态编译的内容。
正如我们将在以下关于向JDK 7靠齐的章节中看到的,Groovy 2.0支持JVM新的"invoke dynamic"指令及其相关API,它们简化了Java平台上动态语言的开发并为Groovy的动态调用带来了额外的性能提高。可不幸的是,在本文撰写时,JDK 7尚未被部署于生产环境,因而并非所有人都有机会运行最新版本。所以期待性能改进的开发者若没法运行在JDK 7上,就不会在Groovy 2.0中看到太多的改变。所幸,Groovy开发团队考虑到了这些开发者(除了其他改进之外)会对性能改进感兴趣,其手段就是允许类型检查后的代码代码可被静态编译。
废话少说,让我们现在就亲手试试新的@CompileStatic
注解:
import groovy.transform.CompileStatic
@CompileStatic
int squarePlusOne(int num) {
num * num + 1
}
assert squarePlusOne(3) == 10
这次使用的是@CompileStatic
,而非@TypeChecked
,并且你的代码会被静态编译,同时生成的字节码非常像javac的字节码,运行速度一样。就像@TypeChecked
注解,@CompileStatic
能注解类和方法,@CompileStatic(SKIP)
可以让某个方法在其所属类被@CompileStatic
标记时不被静态编译。
生成类javac(javac-like)字节码的另一好处是那些被注解的方法的字节码大小会比通常Groovy为动态方法生成的字节码的大小要小,因为要支持Groovy的动态特性,动态场景下的字节码包含了调用Groovy运行时系统的额外指令。
最后一点值得注意的是,框架或库代码作者可使用静态编译,这有助于避免当代码库中多个部分使用动态元编程时的负面影响。像Groovy这类语言中可用的动态特性给开发者带来了极强的能力和灵活性,但鉴于元编程特性是动态发挥作用的,若不加注意,不同的假设会存在于系统的不同部分,由此产生意想不到的后果。举一个例子(虽然有点刻意为之),假设你在使用两个不同的库时发生的情景,两个库都给你的核心类添加了一个名字相似但实现不同的方法。什么行为是期望的?有经验的动态语言使用者可能之前就见过这个问题,并且可能听说它被称为“猴子补丁(monkey patching,译注:在不改变原始代码的情况下扩展或修改动态语言运行时代码的方法)”。若能静态编译代码库中的部分代码 - 那些不需要动态特性的代码 - 保护了你不受猴子补丁的影响,因为静态编译后的代码不会经过Groovy的动态运行系统。尽管语言的动态运行时方面不允许出现在静态编译环境中,但所有常用的AST转换机制还会像以前一样工作良好,因为多数AST转换机制也是在编译时施展它们的魔法。
说到性能,Groovy的静态编译代码通常会或多或少跟javac的一样快。在开发团队使用的一些微基准测试中,有些情况下性能相同,而有时则可能稍慢。
在以前,由于Java和Groovy透明无缝的集成,我们过去常建议开发者优化Java的hotspot例程以获得进一步改进性能,但是现在,有了这个静态编译选择,情况变了,那些想完全用Groovy开发项目的人们也能这样做了。
Groovy编程语言的语法其实来自于Java语法本身,但很明显,Groovy提供了额外漂亮的便捷方法让开发者生产力更高。让Java开发者熟悉的语法一直以来都是这个项目的重要卖点,并且被广泛接纳,这得益于平坦的学习曲线。我们当然也期望Groovy用户和新人也能从Java 7增加的"Project Coin"所提供的一些语法改进中受益。
除了语法,JDK 7还为它的API带来了一些有趣的新事物,这是长久以来的第一次,它甚至添加了一个被称为"invoke dynamic"的字节码指令,它旨在让实现者更容易地开发他们的动态语言和获得更高的性能。
从第一天开始(这要从2003年说起!),Groovy就拥有几处建立在Java之上的语法增强和特性。例如,人们可以想到的是闭包,以及switch/case
语句中可使用的不仅限于离散值,而Java 7中只是多了能使用多个String
。所以一些Project Coin语法增强,比如switch中的多个String
,已经在Groovy中了。然而,有些增强是新的,如二进制字面量、数字字面量中的下划线或者多catch块,Groovy 2都支持。唯一漏掉的Project Coin增强是"try with resources"结构,对于它,Groovy通过Groovy Development Kit丰富的API提供了多个替代解决方案。
在Java 6及之前版本,以及Groovy中,数字可以表示成十进制、八进制和十六进制,而在Java 7和Groovy 2中,你可以使用以“0b”做前缀的二进制符号:
int x = 0b10101111
assert x == 175
byte aByte = 0b00100001
assert aByte == 33
int anInt = 0b1010000101000101
assert anInt == 41285
当写长变量数字时,很难用肉眼分辨出一些数字是如何分组聚合在一起的,例如千位分组,单词等等。通过允许在数字字面量中放置下划线,就很容易区分这些分组了:
long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010
当捕获到异常时,我们通常会复制两个或更多的异常块,因为我们想用同样的方式处理它们。解决方法是,要么在它自己的方法中分离出通用的内容,或者一种更丑陋的方式就是通过捕获Exception
(或者更糟的Throwable
)完成一个捕获所有异常的方法。用多catch块,我们能定义要用一个catch块捕获和处理的多种异常:
try {
/* ... */
} catch(IOException | NullPointerException e) {
/* 一个代码块处理2个异常 */
}
正如本文之前提到的,JDK 7带来了一个被称为"invoke dynamic"的新字节码指令以及相关的API。其目的是帮助动态语言实现者在Java平台之上打造自己的语言,实现手段则是:简化动态方法的调用路径,定义可缓存动态方法的"call site",作为方法指针的"method handles",存储类对象中各种元数据的"class values",以及其他一些内容。不过事先提醒,尽管承诺性能改进,但"invoke dynamic"在JVM内部还没有完全优化,也并不总能提供最好的性能,但随着一步步的更新,优化就会到来。
Groovy带来了它自己的实现技术,用“call site缓存”加速方法的选择和调用,用元类注册库存储元类(类的等价动态运行时),执行跟Java一样快的原生原始计算(native primitive calculation),等等。但随着“invoke dynamic”的问世,我们将重新把Groovy的实现置于这些API和JVM字节码指令之上,以获得性能的改进和简化我们的代码库。
如果有幸运行JDK 7,你就能使用已经编译进"invoke dynamic"支持的Groovy JAR的新版本。很容易辨认那些JAR,因为它们名字都含有"-indy"区分。
然而,要想利用"invoke dynamic",光用"indy"JAR编译你的Groovy代码还不够。鉴于此,使用“groovyc”编译器或者“groovy”命令时,你必须使用--indy标记。这也就意味着,就算用的是indy JAR,你仍可以面向JDK 5或6进行编译。
同样的,如果你正在使用groovyc Ant task编译你的项目,你还可以指定indy属性:
...
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc"
classpathref="cp"/>
...
<groovyc srcdir="${srcDir}" destdir="${destDir}" indy="true">
<classpath>
...
</classpath>
</groovyc>
...
Groovy Eclipse Maven编译器插件还没有更新包含Groovy 2.0支持,但也快了。对于GMaven插件的用户,尽管已经可以配置插件使用Groovy 2.0,目前还没有支持invoke dynamic的标志位。同样,GMaven很快也会有这方面的更新。
当在Java应用中集成Groovy时,用GroovyShell
,你还可以通过给GroovyShell
构造函数传递一个CompilerConfiguration
实例来激活invoke dynamic支持,在GroovyShell
上可以访问和设置优化选项:
CompilerConfiguration config = new CompilerConfiguration();
config.getOptimizationOptions().put("indy", true);
config.getOptimizationOptions().put("int", false);
GroovyShell shell = new GroovyShell(config);
由于invokedynamic被期望成能够完全替代动态方法分发,禁用那些为了优化边缘情况而生成额外二进制码的原始优化(primitive optimizations)是有必要的。即使在某些情况下它比激活原始优化慢,JVM的未来版本将会对JIT有所改进,它将有能力内联(inlining)多数调用并去除那些没必要的装箱(boxing)。
在我们的测试中,我们注意到有些领域取得了有趣的性能改进,而其他程序比没使用invoke dynamic支持的运行慢。然而,Groovy团队在Groovy 2.1的pipeline中取得了进一步的性能改进,但我们注意到JVM还没有微调,全面优化仍然有很长的路要走。但所幸,即将到来的JDK 7的更新(尤其是更新8)应该已经包含了这样的改进,这样的情况必将改善。此外,随着invoke dynamic被用于JDK 8的 Lambdas实现,我们可以保证未来会有更大的改进。
我们将通过模块化介绍,结束这次Groovy 2.0新特性之旅。就像Java,Groovy不只是一门语言,它还是服务于多种用途的API集合:模板、Swing UI构建、Ant脚本、JMX集成、SQL访问、servlet服务等。Groovy的发布版就是把所有这些特性和API打成一个大的JAR。但不是所有人在自己的应用里总是需要所有内容:如果正在写Web应用,你会对模板引擎和Servlet感兴趣,但是如果正在做一个富桌面客户端程序,你可能仅需要Swing构建器。
因此,本版本模块化的第一个目标就是将原始的Groovy JAR真真切切的划分成更小的模块、更小的JAR。核心的Groovy JAR文件现在缩小了一半,我们有如下可用的特性模块:
对于Groovy 2,你现在可以只挑选感兴趣的模块,而不用把所有内容都带入到classpath中。但我们仍提供包含所有内容的“完整”JAR,假如你不想只是为了节省一点空间就要处理复杂的依赖关系的话。我们还为运行在JDK7上的代码提供了用“invoke dynamic”支持选项编译后的JAR文件。
让Groovy变得更模块化的工作也产生了一个有趣的新特性:扩展模块(extension module)。通过将Groovy分裂成更小模块,方便模块扩展方法的机制已经建立。由此,扩展模块可以给其他类,包括来自JDK或第三方库的类,提供实例和静态方法。Groovy用这种机制修饰了来自JDK的类,给诸如String、File
、流以及其他更多的类添加了新的有用方法 - 例如,URL上的getText()
方法,允许你通过HTTP get获得远程URL的内容。还需要注意的是,静态类型检查器和编译器也知道你模块中的这些扩展方法。但先看看如何给现有类型添加新的方法。
要给现有类型添加新的方法,你必须创建一个包含这些方法的帮助类。在这个帮助类中,所有的扩展方法其实都是public
的(这在Groovy中是缺省的,但若用Java实现,就需要标出)和static
的(尽管它们将在类的实例中可用)。它们接受的第一个参数其实总是要在上面调用扩展方法的实例。余下参数将在调用时被传入。这跟Groovy的Category使用的是一样的惯例。
假定我们要给String
添加一个greets()
方法, 它向作为参数传入的人名问好,所以你可以像下面这样写:
assert "Guillaume".greets("Paul") == "Hi Paul, I'm Guillaume"
要实现它,你要创建一个含有这个扩展方法的帮助类,如:
package com.acme
class MyExtension {
static String greets(String self, String name) {
"Hi ${name}, I'm ${self}"
}
}
对于静态扩展方法,用同样的机制和惯例。现在我们给Random添加一个静态方法,获得两个值之间的一个随机整数,你可以按照这个类来处理:
package com.acme
class MyStaticExtension {
static String between(Random selfType, int start, int end) {
new Random().nextInt(end - start + 1) + start
}
}
这样,你可以用如下方式使用这个扩展方法:
Random.between(3, 4)
一旦编写好了包含扩展方法的帮助类(用Groovy或Java),你需要为模块创建描述符。你必须在模块文件夹的META-INF/services
目录下创建一个名为org.codehaus.groovy.runtime.ExtensionModule
的文件。可以定义四个基本属性,告诉Groovy运行时模块的名字和版本,以及用逗号隔开的类名列表,这些类就是为扩展方法写的帮助类。如下是我们最终的模块描述:
moduleName = MyExtension
moduleVersion = 1.0
extensionClasses = com.acme.MyExtension
staticExtensionClasses = com.acme.MyStaticExtension
一旦Classpath中有了这个扩展模块描述符,现在就能在代码中使用这些扩展方法了,不需要import或者其他动作,因为这些扩展方法是自动注册的。
在脚本中使用@Grab注解可以从类似Maven Central这样的Maven库中获取依赖。此外,使用@GrabResolver注解,你还能为依赖指定自己的位置。如果你正通过这种机制“获取”一个扩展模块,扩展方法也会被自动安装。理想情况下,出于一致性考虑,模块名字和版本应该跟制品的id和版本关联。
Groovy在Java开发人员中很流行,并为他们的应用提供了成熟的平台和生态系统。但我们并未满足于现状,Groovy开发团队会一如既往继续提高语言和它的API,帮助用户在Java平台上提高他们的生产率。
Groovy 2.0致力于三个关键主题:
作为SpringSource的Groovy开发主管,VMware部门经理,Guillaume Laforge是官方的Groovy项目经理,领导Codehaus下的Groovy动态语言项目。
他发起创建Grails Web应用框架,建立了Gaelyk项目,一个用Groovy为Google App Engine开发应用的轻量级的工具。他还是频繁在JavaOne、GR8Conf、SpringOne2GX、QCon和Devoxx等大会上介绍Groovy、Grails、Gaelyk、领域建模语言的会议发言人。
Guillaume也是法国French Java/OSS/IT播客LesCastCodeurs的创始成员之一。
原文链接:What’s new in Groovy 2.0?