第二章 Groovy 动态语言
Grail 是一个非常好的框架,它从 Web 应用的开发中减去了很多繁重的工作。你已经看到从零开始创建一个可用的应用并运行它是多么的简单。能有这样的效率,很大一部分要归功于在 Grails 的开发中采用了 Groovy 作为主要语言。
这一章的目的是作为 Java 开发者们的入门资料,它覆盖了在本书后续部分中非常有用的基本知识。进度很快,但是你不必一次就理解所有的东西。在你的经验有所增长后或者你需要深入了解 Groovy 的时候,我们建议你再返回来看一下这一章。如果你已经了解了这门语言,你可能仍想浏览一下本章,因为我们会谈一些你可能没有注意的细节和易犯的错误。
我们从介绍很多 Groovy 和 Java 的小差别开始,这些差别大部分意味着语法上的简单改变。然后在讲解闭包和语言的动态特型之前,我们突出了 Groovy 中没有的 Java 的特性(很少)。甚至如果你快速地读完本书,你具备的知识将已经可以理解这本书中的例子和编写自己的代码了。在这之前,本章已经有足够的信息可以让你成为一个有能力的 Groovy (和 Grails )的开发者。
我们可以直接来解释 Groovy 和 Java 的区别,但是没有一种方式比亲自动手更容易理解一种语言了。带着这种观念,我们将向你介绍一些 grails 命令,让你可以简单安全的实践 groovy 。我们将通过解释两种语言的一些较小但是又很重要的不同点把这种观点贯彻下去,这样,你就可以理解本章后面的例子了。
你已经安装了 Grails ,现在你想尝试下这种叫 groovy 的编程语言。那么你可以做些什么?在第一章,你看到用 create-app 命令去创建一个 grails 项目。还有两种命令提供了交互式的环境来运行 groovy 代码。你可以利用他们来玩转 grails 应用程序。
第一个命令是 shell ,它开启一个交互式的命令处理程序或者命令提示信息,在这里你能输入 groovy 表达式并且查看结果。
shell 对于评估表达式和检查方法调用的结果是非常好的,并且它的功能已经强大到你可以在 shell 里定义新的类。但是要编辑你已经写好的代码是不可能,因此它还是有所限制的。
通过 console 命令开启的 grails 控制台,它是一个 Swing 应用程序,它让编写和运行简单的脚本变得更容易,它甚至提供语法突出显示!控制台包含两部分:上面的部分包含你的 groovy 代码,下面的部分显示运行脚本的结果。这个控制台相对于 shell 的最大优势是你能改变脚本并且随着这些变化立刻运行它。你也能保存脚本在以后再重新载入他们。
注意 :你只能在 grails 项目中开启 shell 和 console 命令。值得指出的是,这两种命令采用了标准的 Groovy shell 和控制台,这解释了在接口中为何提到“ GroovyShell ”和“ GroovyConsole ”。然而, grails 加入一些特殊的方法让你去与你的 grails 项目互动,但却不是通过 groovy 标准的 shell 和控制台。
脚本任务是什么?
为了直接运行 Java 代码,你必须创建一个类并且定义一个静态的 main() 方法。这听起来并不是特别的难,但是它对编写简单应用的限制却惊人的有效。为了去除这个障碍, groovy 提供了脚本:按顺序执行的文本文件中的语句集。你可以将它们看作是 main 方法的方法体,但没有类和方法的定义。
在 grails 控制台试一下这个简单的脚本:
println “ Hello World !”
运行 Script>Run 菜单项或者用 ctrl-r 键盘快捷键去执行脚本。就是这样。你已经编写并执行了一段简单的 groovy 脚本。
现在你知道怎么使用控制台了,你准备好去测试我们将要做的例子。让我们看一下一些你需要去注意的基本语言特性。
除了一些例外,合法的 Java 代码通常也是合法的 groovy 代码。但编写 idiomatic groovy 意味着可以利用它的一些附加特性。我们先从在 Java 中比较熟悉,但是在 groovy 里却有些轻微的变化的概念说起。
更少的导入
正如你所知道的,所有在 java.lang 包里的类都被 Java 自动导入了。 Groovy 通过继承了这个行为包含了下面的这些包:
<!-- [if !supportLists]-->n <!-- [endif]-->java.io
<!-- [if !supportLists]-->n <!-- [endif]-->java.math
<!-- [if !supportLists]-->n <!-- [endif]-->java.net
<!-- [if !supportLists]-->n <!-- [endif]-->java.util
<!-- [if !supportLists]-->n <!-- [endif]-->groovy.lang
<!-- [if !supportLists]-->n <!-- [endif]-->groovy.util
在编写脚本的时候这是很方便,因为你经常结束使用这些核心包中的类,并且运行脚本只为了找到你少导入的那个包变得相当乏味。
断言
这章的许多例子用到了 assert 这个关键字:
assert 1==1
assert ‘c’ !=’z’
如果相应的表达式的值等于 false ,则 assert 做的全部工作是抛出一个 java.lang.AssertionError (符合我们后面看到的 Groovy 真值)。你应该熟悉来自于 Java 中的 assert ,但是当心,虽然它接近和 Java 版本的语义,可是它不可以在 jvm 上用 -ea 参数开启或者关闭。
真值……
你可以期待“ groovy 真值”包含对生活、宇宙,甚至每件事情的答案。很遗憾,你需要到其他地方去寻求答案。它的实际意义非常的乏味,并且涉及到 groovy 看作 true 或者 false 是什么样的。
在 Java 中,条件语句只有在 Bealoon 值时才能工作,这是为冗长的代码准备的:
if(mySet == null || mySet.isEmpty()){ …… }
Groovy 将其他对象强制转成布尔型来帮助减少这种冗长并提高可读性。例如,一个 null 的引用和一个空的集合将都判为 false ,那么同功能的 groovy 的代码看起来是这样的:
if( ! mySet){ …… }
表 2.1 列出了 Groovy 真值使用的强制转换。注意为 null 的对象引用强制转换适用于列出的所有的对象类型,所以为 null 或者为空的 map 对象都被判为 false 。
表 <!-- [if supportFields]><span style='mso-element:field-begin'></span><span style='mso-spacerun:yes'> </span>SEQ <span lang=ZH-CN style='font-family:黑体; mso-ascii-font-family:Arial'>表</span> \* ARABIC <span style='mso-element:field-separator'></span><![endif]-->1 <!-- [if supportFields]><span style='mso-element: field-end'></span><![endif]-->Groovy真值的计算
类型 |
判为 false |
Boolean |
false , Boolean.FALSE |
对象引用 |
Null |
数字 |
0 |
String , GString |
0 长度的字符串 |
Collection |
空集合 |
Map |
空映射 |
Iterator |
hasNext() 返回 false |
Enumeration |
hasMoreElements() 返回 false |
java.util.regex.Matcher |
find() 返回 false |
当使用 groovy 真值时要当心—你检查过这个行为是你真的想要的吗?问问自己是否应该检查是空值还是一些其他值的替代。例如,如果你知道你在测试一个集合并且你想像对待一个空的集合一样去处理一个空的引用时,用 Groovy 真值是合适的。然而,如果你想区别对待一个空的集合和空值时,使用诸如 obj= =null 这样的代码让验证明确些。这将让读代码的人清楚地了解你的意图。
public 是默认的作用域
正如你了解的, Java 有四种作用域: private , protected , public ,和默认(包)作用域。 Groovy 的设计者认为最后一种即默认作用域是多余的,因此它只支持其余 3 种。相应的,这就导致了 public 被定义为默认的作用域,因为它是最常用的。如果一个类、字段或者方法没有显式地声明作用域,它将自动被指定为 public 而不是 Java 默认包作用域。
检查异常不用捕捉
Groovy 中 Error 处理基本和 Java 一样。语法上,异常是统一被 try …… catch …… finally 板块处理。不同是在 groovy 中你不必捕捉检查异常。对于所有的意图和目标来说,这意味着检查异常和运行异常是相同的。
总体上说,这种结果让样板代码更少了,因为你仅仅捕捉你知道怎么去处理的异常就可以了。但是也有不要忘记你正处理检测异常的很重要的情况。例如, Spring 的默认事务行为只检查运行异常。如果此刻这对你还不意味着什么,别着急,只要记住检查异常仍是检查异常,即使你没有捕获他们。
并不是那么糟糕,不是吗?我们现在有基础来接触更多实质的不同,所有的一切都是为了让我们的生活更简单。
你肯定熟悉诸如: + 、- 等等的操作符。 Groovy 支持所有与 Java 中相同的操作符,但它也介绍了一些新的操作符让你的生活更简单,让你的代码更易读。
? . (空安全对象导航符)
你有没有被在引用上调用方法前要检查它是否为空惹怒过? Groovy 开发者就有这种时候,所以他们在语言中加了一个空安全圆点( . )操作符。
想一下 Java 中检查一个对象或者它的一个属性是否为空的常见语法:
if (obj != null && obj.value != null) {
...}
在 Groovy 里你可以这么写:
if ( obj?.value != null ) {…}
在这个例子中, value 字段只有在 obj 不是 null 的时候才能访问。否则整个表达式 obj?.value 被判为 null ——不再有空指针异常!你可以连用这些这种操作符,而且事实上你应该这样。否则,你会看到令人畏惧的异常,所以这么做:
user.company?.address?.state?.name
而不是这样:
user.company?.address.state.name
一旦你在一条链中用了“? . ” ,那么你在链的其余部分也应该使用这个操作符。
?: (埃尔维斯操作符 ! )
Java 和 groovy 都有三目运算符,它是 if …… then …… else 结构的简洁形式:
<condition>?<expr1>:<expr2>
因此,如果这个条件为真,将计算 expr1 ,否则你将计算 expr2 。
这里有个例子是关于这种操作符的常规用法的:
String name = (person.getName() != null) ? person.getName() : “<unknown>”
用语言表述就是,如果 person 的 name 赋值了,我们就获得这个名字。否则我们使用默认值。
因为它用的如此频繁,所以 Groovy 介绍了一种简短的形式:
String name = person.getName() ?:”<unknown>”
这是埃尔维斯操作符,并且如果它是 true (符合 Groovy 的真值)就返回表达式左边的值,否则就是返回表达式右边的值。
为什么叫“ Elvis ”?顺时钟旋转 90 度你就知道了。
*. (展开点操作符)
没有哪个操作符比“ spread-dot ”更好的通过名字伪装了自己。它有什么作用呢?令人惊讶的是,一些有用的东西:它在一个集合里的每一项上调用一个方法,并且它创造了一个新列表,这个列表包含了这些方法获得的结果集。
例如,你有一些对象的一个列表,这些对象都有 getName() 方法,下面的代码将创造一个新的集合,这个集合包含所有原始项的名字:
List names = people*.getName()
这个列表有与原始列表一致的顺序,因此在前面的例子中 names 将和 people 中的顺序一致。这个操作符也能被用在其他类型的集合上,但结果通常都是个列表并且它的项通常是重复原始的集合中的顺序。
我们也可以使用属性符号,这在后面章节会有所介绍:
List names = people*.name
这将会是你在例子和真正的代码中最常见到的这种操作符的用法了。
<=>( 太空船比较符 )
Java 有很多操作符用于比较两个值,那么为什么我们还想要另外一个操作符呢?如果你曾经实现了 Comparable.compareTo() 或者 Comparator.compare() 方法,你就知道为什么了。
这是个整型比较器的内部实现:
public int compare (int i1, int i2){
if(i1==i2) return 0;
else if(i1 < i2) return -1;
else return 1;
}
对数字来说,你可以将代码减少至一行,但它对其他类型就没有这么简单了。那么如果我们想让一个操作符有与 compare() 方法同样的行为,那么这个方法应该是什么样的?让我们看一下:
public int compare(int i1,int i2){
return i1< = > i2;
}
这是 groovy 的“太空船”操作符,它可以用在实现了 Comparable 的任何类型上。它在混合了一些 Groovy 的其他特性方面对自定义比较器和列表排序特别的有用,这些特性在后续章节介绍。如果左边小于右边它将返回一个负值,如果操作数相等将返回 0 ,否则将返回正数。这对于非零结果的绝对值是没有保证的,仅仅符号是有意义的。
= = ( EQUALS() )
当你在 Groovy 中使用 = = 的时候,生成的代码是使用 equals() 方法的。它经常是空值安全的,意思就是不管操作符两边有一个还是都是 null 程序将继续工作。
下面的例子通过用断言和字符串演示了这个操作符的用法:
String str = null
assert str = = null
assert str !=”test”
str = “test”
assert str != null
assert str = = “test”
str += “string”
assert str = = “test string”
这个例子并没有介绍所有的情况,并且对于这个规则会有一个异常:如果左边对象实现了 java.lang.Comparable ,那么 = = 用 compareTo() 方法而不是 equals() 方法。在这种情况下,如果 compareTo() 方法返回 0 结果将是 true ,否则就是 false 。
也许你想知道 Groovy 为什么这么做,记着遵照 BigDecimal.equals() 方法 0.3 和 0.30= 是不相等的,然而按照 compareTo() 方法是相等的。最小意外的原则建议表达式 0.3 = = 0.30 应该是 true ,在 Groovy 中也确实是这样的。
最后一点,那么你如何应对 Java 中的 = = 的功能呢?用 is() 方法:
BigDecimal x = 0.234
BigDecimal y = x
assert y.is(x)
assert !y.is(0.234)
assert y = = 0.234
如你所能见到的,如果两个对象是同样的实例它将只返回 true 。
尽管这些操作符你现在可能不熟悉,但不久后你将在你的代码中自信地使用它们。下面我们看一下 Groovy 的类型体系。