上一篇文章我们从整体上初步认识了Gradle,也说了Gradle的学习曲线,这篇文章我们就按照顺序来讲解Groovy语言。
需要说明的是这篇文章大部分内容是翻译自Groovy的官方文档,不过中间也有些许内容是我自己学习过程中总结整理的,如果大家英语好的话建议直接看英文文档。


这是一个系列的Gradle文章:

Groovy是一门什么语言

在Groovy的官网上有对Groovy的一段描述:

1
Groovy tries to be as natural as possible for Java developers. We’ve tried to follow the principle of least surprise when designing Groovy, particularly for developers learning Groovy who’ve come from a Java background.

大意是说Groovy就是为了JAVA程序员“量身定做”的,既然这么说的话,我们学习Groovy应该是比较容易的。当然事实也是如此,不过Groovy作为一门脚本语言,也有它与JAVA的区别之处,例如在JAVA中我们需要每一行都要分号作为结束标志,但是Groovy却可以省略;我们调用JAVA中的方法时需要使用括号,而Groovy中也可以省略。当然还有很多特点使得Groovy相比JAVA来说都是一门更简洁的语言。接下来我们就来了解一番。

搭建Groovy开发环境

尽管Gradle使用了Groovy作为开发语言,但是确切地说Gradle使用的是基于Groovy的领域特定语言(DSL),也就是说Gradle只是使用了Groovy中的一些语法。所以当我们来了解Groovy时最好还是搭建Groovy的开发环境。

Linux环境下

1
2
3
4
5
6
7
8
9
10
11
12
13
## 到sdkman官网下载对应的安装shell script,然后调用bash解析器去执行
## 在这个过程中最好关闭代理
curl -s get.sdkman.io | bash
## 继续执行下面的命令
source ~/.sdkman/bin/sdkman-init.sh
## 安装Groovy
sdk install groovy
## 查看Groovy版本
groovy -version
Groovy Version: 2.4.14 JVM: 1.8.0_131 Vendor: Oracle Corporation OS: Linux

如果能够成功显示如上的Groovy版本则说明安装成功。

Windows环境下

点击这个连接到Groovy官网下载所需的版本,之后安装并设置环境变量即可。以上步骤完成之后同样在命令行中测试groovy -version,看看能够正确显示Groovy的版本号。

Groovy自带编辑器

使用终端或者命令行执行groovyConsole等待一下即可打开Groovy自带的编辑器,我们可以直接在上面写程序,之后使用快捷键Ctrl+R可以执行代码查看执行结果;使用快捷键Ctrl+W可以清空控制台的输出信息。另外需要了解的一点就是Groovy的代码需要保存在.groovy文件中。

注释

Groovy与JAVA有很多的相同点,在注释上尤为明显,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//单行注释
println "single line comments" //注释可以位于语句后方
/* 多行
注释 */
println "multiline comments" /*多行注释还可以
这样写*/
println 1 /*还可以写在语句中间*/ +2 /*没错,这样写也是可以的*/
//文档注释与JAVA类似
/**
* 类说明
*/
class Person {
/** 变量说明 */
String name
/**
* 说明方法的含义
*
* @param 参数含义
* @return 返回值含义
*/
String greet(String otherPerson) {
"Hello ${otherPerson}"
}
}
//特殊的单行注释,这种注释通常是用来给UNIX系统声明允许脚本运行的类型的
#!/usr/bin/env groovy
println "Hello from the shebang line"

关键字

as assert break case
catch class const continue
def default do else
enum extends false finally
for goto if implements
import in instanceof interface
new null package return
super switch this throw
throws trait true try
while

标识符

普通标识符

标识符以字母,美元符号或下划线开始。不能以一个数字开始。
合法标识符如下:

1
2
3
4
def name
def item3
def with_underscore
def $dollarStart

下面是一些非法标识符:

1
2
3
def 3tier
def a+b
def a#b

特殊一点的就是关键字在.之后的都可以作为合法的标识符:

1
2
3
4
5
foo.as
foo.assert
foo.break
foo.case
foo.catch

引用标识符

引用标识符是指出现在.表达式的后面,例如person.”name”中的name,一般的用法如下:

1
2
3
4
5
6
7
def map = [:]
map."an identifier with a space and double quotes" = "ALLOWED" // 向map添加一个键值对
map.'with-dash-signs-and-single-quotes' = "ALLOWED" // 向map再添加一个键值对
assert map."an identifier with a space and double quotes" == "ALLOWED"
assert map.'with-dash-signs-and-single-quotes' == "ALLOWED"

另外由于Groovy支持多种字符串表达方式(接下来会讲到),所以以下的表达方式也都是允许的:

1
2
3
4
5
6
map.'single quote'
map."double quote"
map.'''triple single quote'''
map."""triple double quote"""
map./slashy string/
map.$/dollar slashy string/$

此外,由于Groovy提供了自己的String,称之为GString,他是一种有插值的字符串(即可以在字符串中引用其它变量),所以以下的这些也算是合法的标识符:

1
2
3
4
def firstname = "Homer"
map."Simpson-${firstname}" = "Homer Simpson"
assert map.'Simpson-Homer' == "Homer Simpson"

字符串

Groovy中的字符串有两大类,第一种与JAVA的一样,即java.lang.String,另一种则是Groovy独有的:groovy.lang.GString,后者的作用就是可以在字符串中引用其他变量(即插值)。

单引号字符串

单引号字符串是普通的java.lang.String,不支持插值。

1
'a single quoted string'

三单引号字符串

三单引号字符串同样也是普通的java.lang.String,也是不支持插值。
三单引号字符串是可以跨行和保留所有的缩进:

1
2
3
4
5
6
7
def startingAndEndingWithANewline = '''
line one
line two
line three
'''
assert startingAndEndingWithANewline.startsWith('\n')
assert startingAndEndingWithANewline.endsWith('\n')

上述的字符串中第一行与最后一行都是换行,可见三单引号字符串是保留了所有的换行,而实际上,Groovy将其中的换行解释为\n,将所有的空白字符则是保留原样。
如果我们想要转义字符的话只要在需要转义的字符前方添加反斜杠\,例如我们想要去掉上方字符串的前后两个换行,则可以这样处理

1
2
3
4
5
6
7
def startingAndEndingWithoutANewline = '''\
line one
line two
line three\
'''
assert !startingAndEndingWithoutANewline.startsWith('\n')
assert !startingAndEndingWithoutANewline.endsWith('\n')

双引号字符串

双引号字符串稍微有点特殊,刚才我们说的插值就是在这里使用的,请看下方的例子:

1
2
3
4
def name = "Guillaume" // 普通字符串
def greeting = "Hello ${name}" // 含有插值的字符串
assert greeting.toString() == 'Hello Guillaume'

可以看到没有含有插值的字符串就是普通的JAVA字符串,含有的则是GString,他可以访问其他的变量,表达的方式有两种:${}$,前者一般用于代替字符串或者表达式,后者则用于A.B的形式中,且也只能适用于这种形式中,如果表达式中含有例如括号,大括号,闭包(稍后讲解)等都是会报错的:

1
2
3
4
5
6
7
8
9
10
11
def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'
def person = [name: 'Guillaume', age: 36]
assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'
def number = 3.14
shouldFail(MissingPropertyException) {
// 这样写是会报错的,因为解释器会将其解释为:"${number.toString}()"
println "$number.toString()"
}

如果想要转义$或者${}占位符,只需要加上反斜杠\就行

1
assert '${name}' == "\${name}"

java.lang.String与groovy.lang.GString配合使用也是我们经常遇到的,在下面的例子中,takeString方法需要的参数是String类型的,但是我们传进去的却是GString类型的,此时GSTring的toString()方法会被调用。使得传入时能够保证类型一致。

1
2
3
4
5
6
7
8
9
10
11
String takeString(String message) {
assert message instanceof String
return message
}
def message = "The message is ${'hello'}"
assert message instanceof GString
def result = takeString(message)
assert result instanceof String
assert result == 'The message is hello'

最后需要注意一点的就是,普通Java字符串是不可变的,而一个GString依赖于插入的值,它的String是可变的。即使有相同的字符串结果,GString和String也没有相同的hashCode,例如:

1
assert "one: ${1}".hashCode() != "one: 1".hashCode()

所以在选择Map的键值时我们应当避免选择GString,看看下面的例子你就清楚了:

1
2
3
4
def key = "a"
def m = ["${key}": "letter ${key}"]
assert m["a"] == null

其他字符串

Groovy中还有另外三种字符串,分别是:三双引号字符串、斜杠字符串、美元符修饰的斜杠字符串,由于这三种字符串用得比较少,这里就略过了,有兴趣的朋友可以直接到Groovy的官方文档中查阅。

列表List

由于Groovy中没有定义任何集合类,因此List也是沿用了java.util.List,其默认的实现则是java.util.ArrayList,不过我们也可以使用as关键字将其转换。
变量定义:List 变量由[ ]定义,比如:

1
def aList = [5,'string',true] //List 由[]定义,其元素可以是任何对象

变量存取:可以直接通过索引存取,而且不用担心索引越界。如果索引超过当前链表长度,List会自动往该索引添加元素

1
2
3
4
5
6
7
8
assert aList[1] == 'string'
assert aList[-1] == true //当索引为-1时表示倒数最后一个
assert aList[-2] == 'string' //当索引为-2时表示倒数第二个,以此类推
assert aList[5] == null //第 6 个元素为空
aList[100] = 100 //设置第 101 个元素的值为 100
assert aList[100] == 100
assert aList[0,2] == [5,true] // 一次访问两个元素,并返回一个包含这两个元素的新列表
assert aList[0..2] == [6,'string',true] // 使用范围访问列表中这个范围内的元素,从start到end元素位置

那么,aList 到现在为止有多少个元素呢?

1
println aList.size() // 结果是 101

现在我们打算将ArrayList转换为其他的具体实现类

1
2
3
4
5
6
7
8
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList // 默认情况下是ArrayLis
def linkedList = [2, 3, 4] as LinkedList // 使用as关键字将其转换为LinkedList
assert linkedList instanceof java.util.LinkedList
LinkedList otherLinked = [3, 4, 5] // 声明的时候直接指定为LinkedList
assert otherLinked instanceof java.util.LinkedList

数组

数组的声明与List类似,且都是用[ ]来表示的,之所以不能像JAVA一样采用{}来初始化数组,是因为在Groovy中{}会被误解为闭包标志。我们需要通过类型转换或者类型定义来定义数组,与List不同的是,数组中的元素类型需要一致,并且数组的大小不能改变:

1
2
3
4
5
6
7
8
9
10
11
12
String[] arrStr = ['Ananas', 'Banana', 'Kiwi'] //可以在定义的时候直接指定为数组,
assert arrStr instanceof String[]
assert !(arrStr instanceof List)
def numArr = [1, 2, 3] as int[] // 使用as关键字转换成数组
assert numArr instanceof int[]
numArr[0] = 3
assert numArr[0] == 3
assert numArr.size() == 3

映射Map

变量定义:Map 变量由[:]定义,比如

1
def aMap = ['key1':'value1','key2':true]

Map 由 [:] 定义,注意其中的冒号。冒号左边是 key,右边是value。在Groovy中key 不一定是字符串,也可以是其他对象,value也可以是任何对象。另外,key 可以用’’或”” 包起来,也可以不用引号包起来。比如:

1
2
3
4
5
6
7
8
9
10
11
12
def aNewMap = [key1:"value",key2:true] //其中的 key1 和 key2 默认被处理成字符串 "key1" 和 "key2"
assert aNewMap."key1" == "value"
```
不过 key 要是不使用引号包起来的话,也会带来一定混淆,比如:
```groovy
def key1="wowo"
def aConfusedMap=[key1:"who am i?"]
//aConfuseMap 中的 key1 到底是"key1"还是变量 key1 的值“wowo”?显然,答案是字符串"key1"。如果要是"wowo"的话,则 aConfusedMap 的定义必须设置成:
def aConfusedMap=[(key1):"who am i?"]

Map 中元素的存取更加方便,它支持多种方法:

1
2
3
4
5
6
7
def aMap = [key1:1,key2:2,key3:3,4:4]
assert aMap.key1 == 1 //这种表达方法好像 key1 就是 aMap 的一个成员变量一样
assert aMap."key1" ==1 //因为我们将key1看成了字符串,因此也可以直接这样访问
assert aMap['key1'] ==1 //这种表达方法更传统一点
assert aMap[4] ==4 //对于数字则需要使用这种方式来访问了
aMap.anotherkey = "i am map" //为 map 添加新元素
assert aMap == [key1:1, key2:2, key3:3, 4:4, anotherkey:"i am map"]

闭包

闭包的定义

闭包(Closure)是Groovy中一种非常重要的数据类型,他是一段可以执行的代码。定义的格式如下:

1
2
def xxx = {paramters -> code} //参数可以是0个或者多个,每个参数之间用`,`分隔开;后面的代码可以一行也可以多行
def xxx = {无参数只有 code} //这种没有参数的情况则不需要->符号

根据上面的格式我们再来看看几个具体的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ item++ } //1
{ println it } //2
{ it -> println it } //3
{ name -> println name } //4
{ -> item++ } //5
{ String x, int y ->
println "hey ${x} the value is ${y}" //6
}
{ String x, int y=6 ->
println "hey ${x} the value is ${y}" //7
}
{ reader ->
def line = reader.readLine() //8
line.trim()
}

上面的几个例子中:

  • 1、闭包中引用了一个item变量,并做自增操作;
  • 2、闭包中直接打印it变量,it变量是闭包中默认的参数,他就是第三行代码的缩减形式;
  • 3、与上一行其实是一样的,这里只是显示的将it变量标了出来;
  • 4、与第三行代码其实是一个道理只是将参数名字改了一下而已;
  • 5、这里没有参数只有一个箭头符号与代码,这样写的意思就是闭包中没有任何参数,连默认的参数都没有,所以如果我们调用闭包的时候传入了参数,那么就会报错;
  • 6、这个闭包定义了两个指定类型的参数,并在代码中使用插值引用了两个参数的值;
  • 7、这个闭包相比上个闭包只是在参数定义上有了区别,在参数定义的时候可以指定默认值;
  • 8、这个闭包定义了一个没有指定类型的参数,并且代码块中是可以有多个表达式的。

除此之外闭包也是groovy.lang.Closure类的实例,尽管它是一个代码块,但是它可以赋给一个变量或者一个属性作为其他的变量,请看如下的实例:

1
2
3
4
5
6
def listener = { e -> println "Clicked on $e.source" } //1
assert listener instanceof Closure
Closure callback = { println 'Done!' } //2
Closure<Boolean> isTextFile = { //3
File it -> it.name.endsWith('.txt') //4
}

上面的几个例子中:

  • 1、闭包是可以赋值给一个变量的;
  • 2、如果不使用def关键字来声明变量,我们可以把闭包赋值给一个groovy.lang.Closure类型的变量;
  • 3、我们可以指定闭包的返回类型,这里的返回类型为Boolean,当然这个是可选的。
  • 4、闭包中代码的最后一行就是闭包的返回值,也可以显示的使用return关键字,也可以不写。

闭包的调用

调用闭包有两种方式:

1
2
3
4
5
6
7
def isOdd = { int i-> i%2 == 1 } //1
assert isOdd(3) == true //2
assert isOdd.call(2) == false //3
def isEven = { it%2 == 0 } //4
assert isEven(3) == false //5
assert isEven.call(2) == true //6

总结来说就是闭包对象.call(参数)或者直接闭包对象.(参数),第4行中是省略了闭包参数的写法,直接使用it来代替,所以我们调用的时候需要传入一个参数。
当闭包作为闭包或方法的最后一个参数,可以将闭包从参数圆括号中提取出来只接在最后;如果闭包或者方法有一个参数且该参数为闭包,那么可以直接省略掉闭包或方法的圆括号;又如果闭包或者方法的参数中有多个闭包,并且这些闭包必须都要排在参数列表中的最后,则此时这些闭包可以依次写在圆括号外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def testClosure={param,c -> c(c(param))} //1
def testMethod(a1, b1, closure){ //2
closure() //调用闭包
}
assert testClosure(5){it*3} == 45 //3
testMethod(1,'test'){println("i am in closure")} //4
def testClosure2={c -> c()} //5
def testMethod2(a1, b1, closure, closure2){ //6
closure() // 调用闭包1
closure2() // 调用闭包2
}
assert testClosure2{"i am in testClosure2"} == "i am in testClosure2" //7
testMethod2(1,'test')
{println("i am in closure")}
{println "i am in closure2"} //8

在上面的例子中:

  • 1、闭包c是作为闭包testClosure的最后一个参数;
  • 2、闭包closure作为方法testMethod的最后一个参数;
  • 3、在调用闭包testClosure时,可以将闭包c写在圆括号外,圆括号内只需要填写其他参数即可;
  • 4、同样的道理,闭包写在圆括号外面。
  • 5、当定义方法或者闭包时只有一个参数,且参数为闭包;
  • 6、当定义的方法或者闭包的参数列表中有多个闭包,并且这些闭包需要排列在参数列表的末尾;
  • 7、调用testClosure2闭包时可以直接省略闭包的圆括号,这种方式也适用于方法;
  • 8、调用testMethod2方法时可以直接将闭包写在圆括号外面,多个闭包以此排列并且不需要使用,将每个闭包分隔开,这种方式也适用于闭包。

闭包的可变参数

闭包可以像其它方法那样声明一个可变的参数列表。当参数列表的最后一个参数的长度是可变的时候(或者是一个数组),那么这个闭包就能接收数量不定的参数,请看下面的实例:

1
2
3
4
5
6
7
8
9
def concat1 = { String... args -> args.join('') } //1
assert concat1('abc','def') == 'abcdef' //2
def concat2 = { String[] args -> args.join('') } //3
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> //4
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'

上面的例子中:

  • 1、闭包可以接收数量可变的String参数;
  • 2、可以在调用时使用任意数量的参数,而且不用被包装成一个数组;
  • 3、使用数组也能达到同样的效果;
  • 4、只要最后一个参数是一个数组或者可变长度的的参数类型即可。

在GString中使用闭包

之前我们讲到GString与传统的java.lang.String的区别,而在GString中使用闭包一般有两种情形:

1
2
3
4
5
6
7
8
9
10
def x = 1
def gs1 = "x = ${x}" //1
assert gs1 == 'x = 1' //2
def gs2 = "x = ${-> x}" //3
assert gs2 == 'x = 1' //4
x = 2 //5
assert gs1 == 'x = 1' //6
assert gs2 == 'x = 2' //7

上面例子中的第一行以及第三行就是在GString中使用闭包的两种情形,严格意义上来讲,第一行代码中的${}并不是闭包,而只是一个插值的符号。
在注释1与注释3中定义了两个GString之后我们在注释2以及注释4中验证了他们的结果,这个结果也确实与我们预料中的一致。
但是接下来的注释5中,我们修改了变量x的值,将其改为2,现在再来看看两个GString的结果,发现采用${}的结果不变,而使用了闭包${->}的则会更新结果,这究竟是为什么呢?
这个主要是因为Groovy对两种符号的加载方式不一样,前者${}名字叫做eagerGString,它的值在GString被创建时就已经确定了,并且一直指向旧的这个对象,当我们去改变他所引用的x对象时,它是不会更新的;而在后者${->}中,Groovy采用了懒加载(lazy evaluation)的方式,即每次GString转换为String的时候都会再次调用闭包,因此它就可以更新。
下面我们再来看一下两个实例加深一下对两者的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam //1
def gs = "Name: ${p}" //2
assert gs == 'Name: Sam' //3
p = lucy //4
assert gs == 'Name: Sam' //5
sam.name = 'Lucy' //6
assert gs == 'Name: Lucy' //7

上面的代码中:

  • 1、首先将对象sam赋值给变量p;
  • 2、使用了插值符号来调用对象的toString()方法;
  • 3、很显然返回的名字是Sam;
  • 4、如果将lucy赋值给p变量;
  • 5、其依旧指向p变量之前指向的旧对象而不会因为p所指的对象的改变而改变GString的值;
  • 6、但是如果我们只是改变了旧对象的属性;
  • 7、那么很显然GString的值还是会更新的。
  • 因此如果使用的是${}的形式,那么改变所指的对象是不能更新GString的值,但是改变对象的属性的话是可以的。
1
2
3
4
5
6
7
8
9
10
11
12
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'

同样的例子,如果是选择了${->}的形式,那么改变所指的对象是可以更新GString的值的,这就是两种形式的区别。

方法

方法的声明

Groovy中的方法与JAVA的差别不大,声明方法时可以加上一个返回值的类型,或者直接使用def关键字而无需标注返回值类型,采用这种方法的时候方法的返回值类型的不固定的。方法可以接收任意数量的参数,并且参数的类型可以是无类型的也可以是有类型的还可以是有默认值的。我们在java中使用的一些修饰方法的关键词都是可以用来修饰Groovy中的方法,但是在Groovy中默认情况下不适用任何访问修饰词的情况下是代表public的。
最后需要再说一下的是,Groovy中的方法总是会返回值的,如果方法中没有return语句,那么方法中的最后一行将会是该方法的返回值,具体的可以看下面的例子。

1
2
3
4
5
6
7
8
def someMethod() { 'method called' } //没有参数也没有return关键字并且使用def来声明方法
String anotherMethod() { 'another method called' } //方法的返回值确定
def thirdMethod(param1) { "$param1 passed" } //方法接收参数,并且参数的类型是未确定的
static String fourthMethod(String param1) { "$param1 passed" } //静态方法,并且参数是有类型的
//方法接收三个参数,第一个是无类型的,第二个是String类型,第三个是int类型并且默认值是6
String fifthMethod(param1,String param2,int param3 = 6) {"$param1,$param2,$param3"}
assert fifthMethod(1,"111") == "1,111,6" //如果调用的时候不传入默认的那个参数,则返回结果就会继续使用默认值
assert fifthMethod(1,"111",5) == "1,111,5" //如果传入了第三个参数,那么默认值将会被覆盖

可变参数的方法

在讲闭包的时候我们已经讲到了可变参数,方法中的可变参数与闭包的一致,这里就不再赘述,下面讲一下两种特殊情况:

  • 当方法接收可变参数时,我们调用方法时传入的参数为null
1
2
3
def foo(Object... args) { args } //1
assert foo(null) == null //2
asser foo(null,null) == [null,null] //3

我们声明的方法foo,他接收一个可变参数,在第二行中我们传入一个null,此时返回的是空,这里的空并不是一个包含null元素的长度为1的数组;但是在第三行中当我们传入了两个null时,却发现返回的确实长度为2的包含两个null元素的数组。这种情况是需要我们在实践中注意的。

  • 当可变函数方法遇到重载方法时
1
2
3
4
5
def foo(Object... args) { 1 }
def foo(Object x) { 2 }
assert foo() == 1 //1
assert foo(1) == 2 //2
assert foo(1, 2) == 1 //3

在这里只需要注意注释2即可,在注释2中我们传入的参数只有一个,但是两个重载的函数都符合我们的参数数量,这个时候Groovy会选择最为确切的方法来调用,两个方法中一个是可变参数,一个是接收一个参数,当然是后者更为确切了,所以后者被调用。因此遇到这种情况时,函数数量最符合的函数将会被调用。

变量

Groovy中的变量我们需要分成两类,一类叫做变量(Field),一类叫做属性(Property),尽管我们在JAVA中可以认为这两个其实就是一种东西,但是在Groovy中他们还是有稍微的区别的。

变量(Field)

  • 变量是可以使用访问修饰符的(如 public,protected,private)
  • 可以使用零个或多个修饰符(如 static,final,synchronized)
  • 类型是可选的,即可以声明类型也可以声明为无类型

如下面的实例:

1
2
3
4
5
6
7
class Data {
private int id =1
protected String description
public static final boolean DEBUG = false
private mapping //最好不要声明为无类型的
private Map<String,String> mapping1 //最好在声明时加上确切的类型
}

尽管Groovy中支持在变量声明的时候声明为无类型,不过这并不是一个好的编程习惯,Groovy官方推荐我们最好在声明时指定类型。

属性(Property)

属性与变量有些许的区别,区别在于它不可以使用访问权限修饰符(public,protected,private),它的声明格式应该如下:

1
2
3
4
class Person {
String name
int age
}

Property与Field的区别在于,前者会默认为属性添加setter和getter方法,例如下面的这个例子就可以看出两者的区别:

1
2
3
4
5
6
7
8
9
10
11
class A {
public String field //1
String property //2
/*
private String property
public void setProperty(String property) { ... }
public String getProperty() { ... }
*/
}

上面的代码中注释1中创建了一个变量,那它就只是创建了一个变量而已;但是注释2中创建了一个属性,而它的作用则相当于接下来三行注释中的代码,即创建了一个private的同名的变量,以及public的setter与public的getter方法。尽管两种声明的方式原理不一样,但是我们访问这两者的方式却是可以一样的,如下所示:

1
2
3
def a = new A()
println a.field //1
println a.property //2

上面的代码中第一行没有疑问,因为field变量是public的,但是第二行中property却是private,那为什么可以这样去访问到它呢?因为实际上当调用a.property时,调用的是a.getProperty()或者是setProperty(String property)
下面再讲一个特殊的情况,就是使用final关键词来修饰的属性(property)是只读的,也就是说他只有getter方法没有setter方法,来看下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
final String name
final int age
Person(String name, int age) {
this.name = name
this.age = age
}
}
def p = new Person('TOM',13)
assert p.name == 'TOM' //1
p.name='TONY' //2

上面的代码中注释1处中的p.name实际上是调用了p.getName()函数,但是注释2处却会报groovy.lang.ReadOnlyPropertyException异常,因为被final修饰的属性是不会生成setter方法的,因此当我们要设置这个变量的值时就会报错。

接下来最后一个需要注意的点是Groovy很贴心的一个功能,即只要我们按照Java Bean的规范声明了属性的getter和setter方法,那么我们就不需要再去声明这个变量了,我们就可以直接用对象.属性的方法来访问这个属性了,具体的方法我们看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PseudoProperties {
// 属性"name"可读可写
void setName(String name) {}
String getName() {'Foo'}
// 属性"age"只可读
int getAge() { 42 }
// 属性"groovy"只可写
void setGroovy(boolean groovy) { }
}
def p = new PseudoProperties()
p.name = 'Foo' //1
assert p.name == 'Foo' //2
assert p.age == 42 //3
p.groovy = true //4

上面的代码中其实声明了三个变量,只是这三个变量的访问权限不一样而已,在注释1处我们可以设置name的值,注释2可以访问name的值,但是我们却只能访问age的值并不能设置它的值,同理我们也只能设置groovy的值但是不能访问。

变量的作用域

在Groovy中变量是有作用域的,普通的Groovy类与JAVA类的变量作用域一致,但是有的时候我们也可以直接写Groovy脚本,即不用定义类直接写代码,这个时候的作用域就不太一样了。下面我们来看看实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovy.transform.Field //不导入则无法使用Field注解
String hello = "hello" //定义变量,作用域是本地域
def world = "world" //定义变量,作用域是本地域
helloworld = "hello world" //全局变量,作用域是绑定域
@Field a = 'I am A' //定义一个全局变量a
void check() {
println hello //1报错
println world //2报错
assert helloworld == "hello world" //3
assert a == "I am A" //4
}
check()

解释上面的代码之前我们需要了解一下在Groovy脚本中定义的变量有两种作用域,分别是绑定域(Binding Scope)本地域(Local Scope)
本地域
本地域是脚本中使用 def 定义的动态类型变量或用特定类型定义的静态类型变量的作用域。
绑定域
绑定域为没有任何前缀修饰的变量的作用域。
例如在上面的代码中,helloworld变量的作用域就是本地域,而helloworld的作用域为绑定域。
两者的区别是什么呢?因为Groovy脚本会被编译成一个类,类名为文件名,该类继承自groovy.lang.Script类,我们刚才讲到的本地域变量会被声明在该类的run()方法内部作为临时变量,它只能在定义它的代码块中被调用,而不能在其他代码块(其他方法)中被调用;绑定域其实也是被声明在run()方法内部的,但是他会被当做参数传递给调用它的方法,所以实际上的我们就可以理解为我们可以在其他方法中调用绑定域的变量。

通过上面的解释,我们可以清楚的看到上面的代码中注释1处和注释2处之所以会报错就是因为访问不到变量,而注释3则是可以访问到的。
还有最后一个就是注释4,首先a变量定义前先导入了Field注解,并在使用的时候显示的引用注解,这个时候a变量就变成了一个全局变量,这是因为a变量在编译之后被声明在了类的层级中,即我们在JAVA中所说的全局变量,既然是全局变量,那么所有的方法都是可以访问到他的。
这里需要注意的就是使用@Field注解与使用绑定域的结果尽管是一致的但是原理却是不同的,我们可以使用jd-gui这个可以将class字节码转换为java代码的软件查看上述代码编译之后的情况来验证我们上面的说法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
...
public class field_scope extends Script
{
Object a;
public field_scope()
{
String str = "I am A";
this.a = str;
}
public field_scope(Binding context)
{
super(context);
String str = "I am A";
this.a = str;
}
public static void main(String[] args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, field_scope.class, args);
}
public Object run()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
String hello = "hello";
Object world = "world";
String str1 = "hello world";
ScriptBytecodeAdapter.setGroovyObjectProperty(str1, field_scope.class, this, (String)"helloworld");
null;
if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))
{
return arrayOfCallSite[1].callCurrent(this);
}
else
{
check();
return null;
}
return null;
}
public void check()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[2].callCurrent(this, arrayOfCallSite[3].callGroovyObjectGetProperty(this));
arrayOfCallSite[4].callCurrent(this, arrayOfCallSite[5].callGroovyObjectGetProperty(this));
ValueRecorder localValueRecorder1 = new ValueRecorder();
try
{
Object tmp64_59 = arrayOfCallSite[6].callGroovyObjectGetProperty(this);
localValueRecorder1.record(tmp64_59, 8);
Object tmp73_64 = tmp64_59;
localValueRecorder1.record(tmp73_64, 8);
boolean tmp87_84 = ScriptBytecodeAdapter.compareEqual(tmp73_64, "hello world");
localValueRecorder1.record(Boolean.valueOf(tmp87_84), 19);
if (tmp87_84)
localValueRecorder1.clear();
else
ScriptBytecodeAdapter.assertFailed(AssertionRenderer.render("assert helloworld == \"hello world\" //3", localValueRecorder1), null);
}
finally { localValueRecorder1.clear(); throw finally; }
ValueRecorder localValueRecorder2 = new ValueRecorder();
try
{
Object tmp139_136 = this.a; localValueRecorder2.record(tmp139_136, 8);
Object tmp148_139 = tmp139_136; localValueRecorder2.record(tmp148_139, 8);
boolean tmp162_159 = ScriptBytecodeAdapter.compareEqual(tmp148_139, "I am A");
localValueRecorder2.record(Boolean.valueOf(tmp162_159), 10);
if (tmp162_159)
localValueRecorder2.clear();
else
ScriptBytecodeAdapter.assertFailed(AssertionRenderer.render("assert a == \"I am A\" //4", localValueRecorder2), null);
}
finally { localValueRecorder2.clear(); throw finally;}
}
}

上面说到Groovy其实是可以不用定义类的,这也符合脚本语言的特性。不过如果是比较复杂的工程,为了能够系统的管理代码,我们还是需要为其定义类。定义类的方法与JAVA类似,但是不需要使用public关键字,因为在Groovy中默认就是public,不仅仅是类,方法与变量也是这样的。我们先用下面的代码来看看Groovy语言的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Language{
def name
def difficulty
String toString(){
"$name,$difficulty"
}
}
def java_lan =new Language()
java_lan.setName "JAVA"
java_lan.difficulty = 5
println java_lan

上面的代码相信大家现在是可以看得懂的啦,下面我们再讲一下关于Groovy中的类其他需要注意的事项:

  • 类中无须定义构造方法

在Groovy的类中,其实有两个默认的构造方法,一个是无参的,一个是带有map参数的构造函数,所以实际上我们有什么函数只需要通过这个带有map参数的函数传递即可,请看下面的实例:

1
2
3
4
5
def java_lan =new Language()
java_lan.setName "JAVA"
java_lan.difficulty = 5
def groovy_lan = new Language(["name":"Groovy","difficulty":5])
  • import的用法

import语句与JAVA很相似,但是也增加了一些新的特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//包声明
package com.bob.groovy.test
//import语句1
import groovy.json.JsonBuilder
def json1 = new JsonBuilder()
//import语句2
import groovy.json.*
def json2 = new JsonBuilder()
//import语句3,使用as关键字取别名
import static Calendar.getInstance as now
assert now().class == Calendar.getInstance().class

不过要特别注意,Groovy与Java类似,已经帮我们默认导入了一些常用的包,所以在我们使用这些包的类时就不用再像上面那样导入了,如下是自动导入的包列表:

1
2
3
4
5
6
7
8
import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal