Groovy 开发工具包
1. 处理 IO 操作
Groovy 提供了很多辅助方法,尽管可以使用 Java 来解决,但 Groovy 还是更多方便的方法来处理文件、流、阅读器,等等。
你特别需要注意添加到这些类中的方法:
java.io.File
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.htmljava.io.InputStream
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.htmljava.io.OutputStream
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.htmljava.io.Reader
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.htmljava.io.Writer
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.htmljava.nio.file.Path
类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
下面,使用上面提供的辅助函数来介绍一些简单而又符合 Groovy 语言习惯的构建范例,更多的有关方法可参看 GDK API。
1.1 读取文件
作为第一个范例,我们来看一看如何将文本文件的所有行都打印出来:
new File(baseDir, 'haiku.txt').eachLine { line ->
println line
}
eachLine
方法是由 Groovy 自动添加到 File
文件上的方法。它可以有多个变体,假如你需要知道行号,可以使用下面这个变体:
new File(baseDir, 'haiku.txt').eachLine { line, nb ->
println "Line $nb: $line"
}
假如因为某种原因,eachLine
语句体内抛出了异常,方法需要确定资源是否正确关闭。这一点对于所有 Groovy 添加的 I/O 资源方法来说都是适用的。
比如在一些情况下,你更喜欢用 Reader
,但仍然会受益于 Groovy 的自动资源管理。在下面这个例子中,即使发生异常,Reader 也会关闭。
def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
while (reader.readLine()) {
if (++count > MAXSIZE) {
throw new RuntimeException('Haiku should only have 3 verses')
}
}
}
有时,你可能会需要将某个文本文件的行放入列表中,可以这样写:
def list = new File(baseDir, 'haiku.txt').collect {it}
或者,你甚至还可以利用 as
操作符将获得的文件内容放到一个关于行的数组中:
def array = new File(baseDir, 'haiku.txt') as String[]
你还记得有多少次必须将文件内容用 byte[]
放到一个字节数组中而要写多少代码吗?现在,Groovy 将一切都简化了:
byte[] contents = file.bytes
处理 I/O 并不限于操作文件。实际上,很多操作都依赖于输入输出流,这就是 Groovy 为何要重新添加大量方法的原因。关于这些,可以查看详细的文档。
再次举例,我们可以非常轻松地从一个 File
中获取 InputStream
:
def is = new File(baseDir,'haiku.txt').newInputStream()
// 省略的逻辑语句......
is.close()
但是,从上面的代码我们也可以看到,你还需要手动关闭这个输入流的。在 Groovy 中,一般最好使用 withInputStream
习语,它可以替你处理这个关闭操作:
new File(baseDir,'haiku.txt').withInputStream { stream ->
// 省略的逻辑语句......
}
1.2 写入文件
在某些情况下,你可能只需要写入文件,而不需要读取。这时,使用 Writer
是一种不错的方法:
new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
writer.writeLine 'Into the ancient pond'
writer.writeLine 'A frog jumps'
writer.writeLine 'Water’s sound!'
}
但对于这么简单的一个例子来说,使用 <<
无疑就够用了:
new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''
当然我们并不一定总是处理文本内容,使用 Writer
还可以直接写入字节:
file.bytes = [66,22,11]
无疑,你也可以直接处理输出流,比如下面这个例子就通过创建输出流,再将其写入一个文件:
def os = new File(baseDir,'data.bin').newOutputStream()
// 省略的逻辑语句......
os.close()
但我们再次看到,需要手动编写关闭输出流的语句,与输入流的情况相同,一般在 Groovy 中最好使用 withOutputStream
习语,因为它既可以自动处理异常,也能在任何情况下关闭输出流。
new File(baseDir,'data.bin').withOutputStream { stream ->
// 省略的逻辑语句......
}
1.3 遍历文件树
在编写上下文脚本时,常见的一个任务就是遍历文件树查找某些特定文件,然后对它们进行一定的处理。Groovy 为此提供了多种方法。例如,你可以对某个目录的所有文件执行一些操作:
dir.eachFile { file -> // 1⃣️
println file.name
}
dir.eachFileMatch(~/.*\.txt/) { file -> // 2⃣️
println file.name
}
1⃣️ 对目录中已找到的所有文件执行闭包代码。
2⃣️ 对匹配特定模式的目录中的文件执行闭包代码。
另外,我们往往必须处理较深层次的文件,这种情况下可以使用 eachFileRecurse
:
dir.eachFileRecurse { file -> // 1⃣️
println file.name
}
dir.eachFileRecurse(FileType.FILES) { file -> // 2⃣️
println file.name
}
1⃣️ 对目录中已找到的所有文件或目录递归地执行闭包代码。
2⃣️ 只在文件上执行闭包代码,但并不递归。
要想使用更复杂的遍历技术,你可以使用 traverse
方法,它需要你设置特定的标志来确定遍历的行为:
dir.traverse { file ->
if (file.directory && file.name=='bin') {
FileVisitResult.TERMINATE // 1⃣️
} else {
println file.name
FileVisitResult.CONTINUE // 2⃣️
}
}
1⃣️ 如果当前文件是目录或者它的名称为 bin
,则停止遍历。
2⃣️ 如果不满足 1⃣️ 条件,则打印文件名并继续遍历。
1.4 数据和对象
在 Java 中,利用 java.io.DataOutputStream
和 java.io.DataInputStream
来序列化与反序列化数据的情况并不少见。Groovy 使得(反)序列化处理更容易了。比如可以利用下面这些代码来将数据序列化到文件中,然后再将文件反序列化:
boolean b = true
String message = 'Hello from Groovy'
// 将文件序列化到文件中
file.withDataOutputStream { out ->
out.writeBoolean(b)
out.writeUTF(message)
}
// ...
// 然后再重新把它读取出来
file.withDataInputStream { input ->
assert input.readBoolean() == b
assert input.readUTF() == message
}
同样,如果想要序列化的数据实现了 Serializable
接口,你可以像下面这样利用一个对象输出流:
Person p = new Person(name:'Bob', age:76)
// 将文件序列化到文件中
file.withObjectOutputStream { out ->
out.writeObject(p)
}
// ...
// 然后再重新把它读取出来
file.withObjectInputStream { input ->
def p2 = input.readObject()
assert p2.name == p.name
assert p2.age == p.age
}
1.5 执行外部进程
前面几节介绍了在 Groovy 中处理文件、Reader 或流的便利性,但在系统管理或开发运维领域中,我们经常还需要与外部进程进行通信。
Groovy 执行命令行进程的方式非常简单,只需把命令行写成字符串的形式,然后调用 execute()
方法即可。比如,在 nix 系统的机器上(或者是 Windows 机器上安装了合适的 nix 命令),可以执行类似下面的命令:
def process = "ls -l".execute() // 1⃣️
println "Found text ${process.text}" // 2⃣️
1⃣️ 在外部进程中执行 ls
命令。
2⃣️ 利用命令输出获取文本。
execute()
方法返回一个 java.lang.Process
实例,该实例随后能允许执行 in/out/err 流,检查处理得到的结束值,等等。
比如,下例中的命令与上例相同,但这次我们每次只处理一行结果流:
def process = "ls -l".execute() // 1⃣️
process.in.eachLine { line -> // 2⃣️
println line // 3⃣️
}
1⃣️ 在外部进程中执行 ls
命令。
2⃣️ 对于进程的输入流的每一行......
3⃣️ 打印改行。
上例中,值得我们注意的是,in
对应着一个输入流标准输出。out
则引用一个将数据发送处理(它的标准输入)的流。
另外一点要记住的是,很多命令都是 shell 内置的,需要特殊处理。假设在 Windows 系统机器上想用以下方式获取文件列表:
def process = "dir".execute()
println "${process.text}"
你会得到这样一个 IOException
异常:Cannot run program "dir": CreateProcess error=2, The system cannot find the file specified.(无法运行程序 “dir”:CreateProcess error=2,系统无法找到指定目录。)
这是因为 dir
是 Windows shell(cmd.exe
)的内建命令,无法以一个简单的可执行文件的形式运行。你需要这样写:
def process = "cmd /c dir".execute()
println "${process.text}"
另外,由于该功能当前其实秘密使用了 java.lang.Process
这个类,所以就必须要提防该类的一些不足和缺点。特别要注意的是,在 java 文档中对该类有如下这番说明:
> 由于一些原生平台上所提供的输出和输入流的缓冲区十分有限,所以如果未能及时地写入子进程的输入流或读取子进程的输出流,可能会导致子进程的阻塞甚至死锁。
正是由于这个特点,Groovy 提供了一些额外的帮助方法来使进程流的控制更为容易。
下面这个例子说明了如何获取进程中的所有输出(包括错误流输出):
def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()
目前存在很多 consumeProcessOutput
的变体形式,它们可以使用 StringBuffer
、InputStream
及 OutputStream
,等等。要想获取完整的信息,请参考 java.lang.Process
的 GDK API。
另外,pipeTo
命令(映射到 |
以便允许过载)可以把某一个进程的输出流提供给另一个进程的输入流。
下面介绍一些有关它的用例:
管道实例:
proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
println proc4.err.text
} else {
println proc4.text
}
严重的错误用法:
def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"
2. 使用集合
Groovy 为多种集合提供了原生支持,这些类型的集合包括:list、map(映射) 或 range(范围)。它们大多基于 java 集合类型,并且附带有 GDK中的一些额外方法。
2.1 List(列表)
2.1.1 列表字面量
你可以按照如下方式创建 lists,注意 []
是空 list 表达式。
def list = [5, 6, 7, 8]
assert list.get(2) == 7
assert list[2] == 7
assert list instanceof java.util.List
def emptyList = []
assert emptyList.size() == 0
emptyList.add(5)
assert emptyList.size() == 1
每一个 list 表达式都创建了一个 java.util.List
接口的实现。
当然也可以把 list 作为构建其他 list 的来源:
def list1 = ['a', 'b', 'c']
//基于 list1 中的项构建一个新的列表
def list2 = new ArrayList<String>(list1)
assert list2 == list1 // == 用来检查两个列表中的每个相应元素是否相同
// 也可以调用 clone()
def list3 = list1.clone()
assert list3 == list1
列表其实是一个有序的对象集合:
def list = [5, 6, 7, 8]
assert list.size() == 4
assert list.getClass() == ArrayList // 特定类型的列表
assert list[2] == 7 // 起始索引为 0
assert list.getAt(2) == 7 // 下标操作符 [] 的等效方法
assert list.get(2) == 7 // 另一种方法
list[2] = 9
assert list == [5, 6, 9, 8,] // 允许末尾出现逗号
list.putAt(2, 10) // 当数值发生变动时,[] 的等效方法
assert list == [5, 6, 10, 8]
assert list.set(2, 11) == 10 // 返回旧值的另一种方法
assert list == [5, 6, 11, 8]
assert ['a', 1, 'a', 'a', 2.5, 2.5f, 2.5d, 'hello', 7g, null, 9 as byte]
// 对象的类型可以不同,对象也允许重复。
assert [1, 2, 3, 4, 5][-1] == 5 // 使用负数代表从末尾开始计算索引
assert [1, 2, 3, 4, 5][-2] == 4
assert [1, 2, 3, 4, 5].getAt(-2) == 4 // getAt() 允许出现负数索引......
try {
[1, 2, 3, 4, 5].get(-2) // 但负数索引不允许用于 get() 方法
assert false
} catch (e) {
assert e instanceof ArrayIndexOutOfBoundsException
}
2.1.2 作为布尔表达式的列表
可以将列表用作 boolean
值:
assert ![] // 空列表的结果是 false
//所有其他列表,不管其中包含的内容,其布尔值都是 true
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]
2.1.3 列表中的迭代
通常,列表中的迭代需要调用 each
和 eachWithIndex
方法来实现,在列表中的每一项上都将执行一定的代码:
[1, 2, 3].each {
println "Item: $it" // `it` 是个很隐含的参数,对应着当前元素
}
['a', 'b', 'c'].eachWithIndex { it, i -> // `it` 是当前元素,而 `i` 则是索引
println "$i: $it"
}
除了迭代之外,往往还用到将列表中的每个元素转换成其他内容,从而创建一个新的列表。这种操作常被称为映射(mapping),在 Groovy 中利用 collect
方法来实现:
assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]
// 代替 `collect` 的快捷格式
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }
def list = [0]
// 有可能要给 `collect` 提供收集元素的列表
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]
2.1.4 操纵列表
过滤与搜索
Groovy Development Kit中包含着与集合相关的大量方法,这些实用性的方法大大增强了集合的标准操作。其中一些方法如下所示:
assert [1, 2, 3].find { it > 1 } == 2 // 查找符合规则的第一个元素
assert [1, 2, 3].findAll { it > 1 } == [2, 3] // 查找符合规则的所有元素
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf { // 查找符合规则的第一个元素的索引
it in ['c', 'e', 'g']
} == 2
assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2 // 返回索引
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // 索引 -1 意指该值并不在列表中
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4
assert [1, 2, 3].every { it < 5 } // 如果所有元素都符合断言,则返回 true
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 } // 只要有元素符合断言,就返回 true
assert ![1, 2, 3].any { it > 3 }
assert [1, 2, 3, 4, 5, 6].sum() == 21 // 利用 plus() 方法对任何数值进行加和运算
assert ['a', 'b', 'c', 'd', 'e'].sum {
it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
// sum 中使用的自定义值
} == 15
assert ['a', 'b', 'c', 'd', 'e'].sum { ((char) it) - ((char) 'a') } == 10
assert ['a', 'b', 'c', 'd', 'e'].sum() == 'abcde'
assert [['a', 'b'], ['c', 'd']].sum() == ['a', 'b', 'c', 'd']
// 可以提供一个初始化值
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006
assert [1, 2, 3].join('-') == '1-2-3' // 字符串连接
assert [1, 2, 3].inject('counting: ') {
str, item -> str + item // 减少操作
} == 'counting: 123'
assert [1, 2, 3].inject(0) { count, item ->
count + item
} == 6
下面是 Groovy 中惯用的一种寻找集合中最大值与最小值的方法:
def list = [9, 4, 2, 10, 5]
assert list.max() == 10
assert list.min() == 2
// 跟任何类似的可对比对象一样,我们也可以比较单个的字符
assert ['x', 'y', 'a', 'z'].min() == 'a'
// 可以使用闭包来指定排序行为
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list2.max { it.size() } == 'xyzuvw'
assert list2.min { it.size() } == 'z'
除了闭包之外,还可以使用 Comparator
来定义比较条件:
Comparator mc = { a, b -> a == b ? 0 : (a < b ? -1 : 1) }
def list = [7, 4, 9, -6, -1, 11, 2, 3, -9, 5, -13]
assert list.max(mc) == 11
assert list.min(mc) == -13
Comparator mc2 = { a, b -> a == b ? 0 : (Math.abs(a) < Math.abs(b)) ? -1 : 1 }
assert list.max(mc2) == -13
assert list.min(mc2) == -1
assert list.max { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -13
assert list.min { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -1
添加或去除元素
我们可以使用 []
来指定一个新的空列表,然后用 <<
将列表项添加进去:
def list = []
assert list.empty
list << 5
assert list.size() == 1
list << 7 << 'i' << 11
assert list == [5, 7, 'i', 11]
list << ['m', 'o']
assert list == [5, 7, 'i', 11, ['m', 'o']]
// `<<` 链中的第一项是目标列表
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]
// 使用 `leftShift` 等同于使用 `<<`
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))
我们可以用多种方式为列表添加元素:
assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// 等于调用 `plus` 方法
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]
def a = [1, 2, 3]
a += 4 // 创建一个新列表,并以这种方式为 `a` 添加新的元素
a += [5, 6]
assert a == [1, 2, 3, 4, 5, 6]
assert [1, *[222, 333], 456] == [1, 222, 333, 456]
assert [*[1, 2, 3]] == [1, 2, 3]
assert [1, [2, 3, [4, 5], 6], 7, [8, 9]].flatten() == [1, 2, 3, 4, 5, 6, 7, 8, 9]
def list = [1, 2]
list.add(3)
list.addAll([5, 4])
assert list == [1, 2, 3, 5, 4]
list = [1, 2]
list.add(1, 3) // 把 3 添加到索引为 1 的元素之前
assert list == [1, 3, 2]
list.addAll(2, [5, 4]) //将 [5,4] 添加到索引为 2 的元素之前
assert list == [1, 3, 5, 4, 2]
list = ['a', 'b', 'z', 'e', 'u', 'v', 'g']
list[8] = 'x' // `[]` 可以当操作符使用,从而按需扩展列表
// 如果需要,也可以在列表中添加 null 值
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']
但要提请大家注意的是,列表中的 +
操作符生成的结果是不可变的。与 <<
操作符相比,使用它会创建一个新列表,而这往往不是你想要的结果,容易引起性能问题。
Groovy Development Kit 中还包含了一些可以通过值来去除列表元素的方法:
assert ['a','b','c','b','b'] - 'c' == ['a','b','b','b']
assert ['a','b','c','b','b'] - 'b' == ['a','c']
assert ['a','b','c','b','b'] - ['b','c'] == ['a']
def list = [1,2,3,4,3,2,1]
list -= 3 // 从原始列表中去除 `3` 创建一个新列表
assert list == [1,2,4,2,1]
assert ( list -= [2,4] ) == [1,1]
也可以按照索引来去除列表元素,这时列表会发生变化:
def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3 // 去除第 3 个元素,然后返回该元素
assert list == [1,2,4,5,6,2,2,1]
如果你只想去除列表中第一个跟值相同的元素,而不是所有元素的话,可以使用 remove
方法:
def list= ['a','b','c','b','b']
assert list.remove('c') // 去除 'c' 并返回 true,因为元素已经清除了
assert list.remove('b') // 去除第一个 'b' 并返回 true,因为元素已经清除了
assert ! list.remove('z') // 返回 false,因为没有这个元素可供清除
assert list == ['a','b','b']
去除列表中所有元素,可以使用 clear
方法:
def list= ['a',2,'c',4]
list.clear()
assert list == []
集合操作
Groovy Development Kit 中还包含了一些便于操作集合的方法:
assert 'a' in ['a','b','c'] // 如果某一元素属于该列表,则返回 true
assert ['a','b','c'].contains('a') // 等同于 Java 中的 `contains` 方法
assert [1,3,4].containsAll([1,4]) // `containsAll` 将检查是否已经找到所有的元素
assert [1,2,3,3,3,3,4,5].count(3) == 4 // 计算匹配相应值的元素的数目
assert [1,2,3,3,3,3,4,5].count {
it%2==0 // 计算符合谓语要求的元素数目
} == 2
assert [1,2,4,6,8,10,12].intersect([1,3,6,9,12]) == [1,6,12]
assert [1,2,3].disjoint( [4,6,9] )
assert ![1,2,3].disjoint( [2,4,6] )
排序
操作集合往往会用到排序。Groovy 提供了多种列表排序方法,从使用闭包到比较器,如下所示:
assert [6, 3, 9, 2, 7, 1, 5].sort() == [1, 2, 3, 5, 6, 7, 9]
def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list.sort {
it.size()
} == ['z', 'abc', '321', 'Hello', 'xyzuvw']
def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]
assert list2.sort { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } ==
[-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]
Comparator mc = { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 }
// 只适用于 JDK 8+
// list2.sort(mc)
// assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]
def list3 = [6, -3, 9, 2, -7, 1, 5]
Collections.sort(list3)
assert list3 == [-7, -3, 1, 2, 5, 6, 9]
Collections.sort(list3, mc)
assert list3 == [1, 2, -3, 5, 6, -7, 9]
复制元素
Groovy Development Kit 中利用操作符重载来提供列表元素的复制方法。
assert [1, 2, 3] * 3 == [1, 2, 3, 1, 2, 3, 1, 2, 3]
assert [1, 2, 3].multiply(2) == [1, 2, 3, 1, 2, 3]
assert Collections.nCopies(3, 'b') == ['b', 'b', 'b']
// JDK 的 nCopies 的语义跟列表所用的 multiply 截然不同
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] // 而不是 [1,2,1,2]
2.2 Maps
2.2.1 Map 字面量
在 Groovy 中,Map(也被称为关联数组)可以通过map 的字面格式([:]
)来创建:
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.get('name') == 'Gromit'
assert map.get('id') == 1234
assert map['name'] == 'Gromit'
assert map['id'] == 1234
assert map instanceof java.util.Map
def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.put("foo", 5)
assert emptyMap.size() == 1
assert emptyMap.get("foo") == 5
Map 的键默认都是字符串:[a:1]
和 ['a':1]
是相等的。这个特点有时可能会让人感到困扰,比如当你定义了一个名为 a
的变量,而又想把 a
的值定义为 map 中的键时。在这种情况下,你必须用括号把键转义,如下所示:
def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null // 没有找到 `Bob`
assert ages['a'] == 43 // 因为 `a` 是一个字面量!
ages = [(a): 43] // 现在用括号把 `a` 转义
assert ages['Bob'] == 43 // 就找到 `Bob` 了!
除了 map 字面量之外,还可以克隆 map,获得一个新的 map 副本:
def map = [
simple : 123,
complex: [a: 1, b: 2]
]
def map2 = map.clone()
assert map2.get('simple') == map.get('simple')
assert map2.get('complex') == map.get('complex')
map2.get('complex').put('c', 3)
assert map.get('complex').get('c') == 3
结果的 map 是原始 map 的浅层副本,如前例所示。
2.2.2 Map 属性表示法
Map 也可以像 bean 那样,只要键是字符串这种有效的 Groovy 标识符,就可以使用属性表示法来获取/设置 Map
中的元素。
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit' // 可以替代 map.get('Gromit')
assert map.id == 1234
def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5
注意:map.foo
有意被设计成一直搜索 map 中的 foo
键。这意味着在一个不含有 class
键的 map 中,foo.class
将会返回 null
。如果你真的想知道类的话,必须使用 getClass()
方法。
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.class == null
assert map.get('class') == null
assert map.getClass() == LinkedHashMap // 这可能是你想要的
map = [1 : 'a',
(true) : 'p',
(false): 'q',
(null) : 'x',
'null' : 'z']
assert map.containsKey(1) // 1 不是标识符,所以可以这样用
assert map.true == null
assert map.false == null
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z'
assert map.get(null) == 'x'
2.2.3 对 map 的迭代
Groovy Development Kit,对 map 惯用的迭代会使用 each
和 eachWithIndex
两种方法。值得注意的是,使用 map 字面量标记定义的 map 都是有序的。也就是说,如果你迭代 map 中的元素,元素项绝对会按照它们添加进 map 的顺序返回。
def map = [
Bob : 42,
Alice: 54,
Max : 33
]
// `entry` 是一个 map 项
map.each { entry ->
println "Name: $entry.key Age: $entry.value"
}
// `entry` 是一个 map 项,`i` 是 map 中的索引
map.eachWithIndex { entry, i ->
println "$i - Name: $entry.key Age: $entry.value"
}
// 当然你还可以直接利用键与值来进行迭代
map.each { key, value ->
println "Name: $key Age: $value"
}
// Key、value 和 i 都可以作为 map 中的索引
map.eachWithIndex { key, value, i ->
println "$i - Name: $key Age: $value"
}
2.2.4 操纵 map
添加或去除元素
为 map 添加元素,可以使用 put
方法,下标运算符,或使用 putAll
:
def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']
def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides)
assert result == [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']
去除 map 中所有元素,可以使用 clear
方法:
def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]
使用 map 字面量格式生成的 map 使用的是 equals
对象和 hashcode
方法。这意味着你不能使用哈希值容易改变的对象,或者说你无法再找回关联值。
另外值得一提的是,你永远不应使用 GString
作为 map 的键,因为 GString
的哈希值跟相应的 String
的哈希值是不同的。
def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
键、值与项
这里集中审视一下键、值与项的关系。
def map = [1:'a', 2:'b', 3:'c']
def entries = map.entrySet()
entries.each { entry ->
assert entry.key in [1,2,3]
assert entry.value in ['a','b','c']
}
def keys = map.keySet()
assert keys == [1,2,3] as Set
上述代码所返回的变异值(map 项、键或值)是非常令人失望的,因为该操作的成功直接跟操作的 map 类型有关。尤其关键的是,Groovy 依靠 JDK 的集合,所以一般无法保证能够安全地通过 keySet
、entrySet
,或 values
来操控集合。
过滤与搜索
Groovy development kit 所包含的过滤、搜索及收集方法跟 lists 中的差不多。
def people = [
1: [name:'Bob', age: 32, gender: 'M'],
2: [name:'Johnny', age: 36, gender: 'M'],
3: [name:'Claire', age: 21, gender: 'F'],
4: [name:'Amy', age: 54, gender:'F']
]
def bob = people.find { it.value.name == 'Bob' } // 查找单独的一个项
def females = people.findAll { it.value.gender == 'F' }
// 都能返回项,但可以使用 `collect` 来获取一些信息,比如ages
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
it.value.age
}
assert ageOfBob == 32
assert agesOfFemales == [21,54]
// 但你也可以使用键/值对作为闭包的参数
def agesOfMales = people.findAll { id, person ->
person.gender == 'M'
}.collect { id, person ->
person.age
}
assert agesOfMales == [32, 36]
// 如果所有项都匹配谓语,`every` 就返回 true
assert people.every { id, person ->
person.age > 18
}
// 如果所有项都匹配谓语,`any` 就返回 true
assert people.any { id, person ->
person.age == 54
}
Grouping
我们利用一些规则将列表中的各项分组归入一个新创建的 map 中,如下所示:
assert ['a', 7, 'b', [2, 3]].groupBy {
it.class
} == [(String) : ['a', 'b'],
(Integer) : [7],
(ArrayList): [[2, 3]]
]
assert [
[name: 'Clark', city: 'London'], [name: 'Sharma', city: 'London'],
[name: 'Maradona', city: 'LA'], [name: 'Zhang', city: 'HK'],
[name: 'Ali', city: 'HK'], [name: 'Liu', city: 'HK'],
].groupBy { it.city } == [
London: [[name: 'Clark', city: 'London'],
[name: 'Sharma', city: 'London']],
LA : [[name: 'Maradona', city: 'LA']],
HK : [[name: 'Zhang', city: 'HK'],
[name: 'Ali', city: 'HK'],
[name: 'Liu', city: 'HK']],
]
2.3. 范围(Ranges)
利用范围可以创建一列连续的值。它们可以像 List
那样使用,因为 Range
继承自 java.util.List。
用 ..
标记法定义的范围是全包含的(包含首尾两个值的列表)。
用 ..<
标记法定义的范围是半包含的,即只含有起始值,而不包含末尾值。
// 全包含范围
def range = 5..8
assert range.size() == 4
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert range.contains(8)
// 半包含范围
range = 5..<8
assert range.size() == 3
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert !range.contains(8)
//不使用索引,获取范围的末尾值
range = 1..10
assert range.from == 1
assert range.to == 10
注意,int 类型的范围实现的效率高,创建了一个轻量级的包含了首尾值的 Java 对象。
任何实现了 java.lang.Comparable 接口用于相互比较的 Java 对象都可以使用范围。可以用 next()
和 previous()
来返回后一个/前一个的范围项。例如,我们可以创建一定范围的 String
元素:
// 全包含范围
def range = 'a'..'d'
assert range.size() == 4
assert range.get(2) == 'c'
assert range[2] == 'c'
assert range instanceof java.util.List
assert range.contains('a')
assert range.contains('d')
assert !range.contains('e')
你还可以利用一个经典的 for
循环来在范围中迭代:
for (i in 1..10) {
println "Hello ${i}"
}
但你还可以采用 Groovy 更惯用的风格来做,利用 each
方法来迭代一个范围:
(1..10).each { i ->
println "Hello ${i}"
}
范围还可以用在 switch
语句中:
switch (years) {
case 1..10: interestRate = 0.076; break;
case 11..25: interestRate = 0.052; break;
default: interestRate = 0.037;
}
2.4 对集合(collection)的一些语法增强
2.4.1 GPath 支持
对于列表和 map 都支持属性标记法,Groovy 提供了语法糖,使得处理内嵌集合变得非常方便,如下例所示:
def listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22]]
assert listOfMaps.a == [11, 21] //GPath 标记
assert listOfMaps*.a == [11, 21] //散布点标记
listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // 也适合 null 值
assert listOfMaps*.a == listOfMaps.collect { it?.a } // 等价标记
// 但只收集 非 null 值
assert listOfMaps.a == [11,21]
2.4.2 散布操作符
散布操作符还可用于将一个集合内联到另一个中。它是一个语法糖,因为通过它能够避免调用 putAll
,有利于实现单行方式。
assert [ 'z': 900,
*: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
// 在 map 定义中 散布 map 标记法
assert [*: [3: 3, *: [5: 5]], 7: 7] == [3: 3, 5: 5, 7: 7]
def f = { [1: 'u', 2: 'v', 3: 'w'] }
assert [*: f(), 10: 'zz'] == [1: 'u', 10: 'zz', 2: 'v', 3: 'w']
//在函数参数中的散布 map 标记法
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30
f = { m, i, j, k -> [m, i, j, k] }
//使用散布 map 标记法来处理未命名与已命名参数
assert f('e': 100, *[4, 5], *: ['a': 10, 'b': 20, 'c': 30], 6) ==
[["e": 100, "b": 20, "c": 30, "a": 10], 4, 5, 6]
2.4.3 星号(*
)操作符
通过星号操作符这个快捷操作符,你可以对集合中所有的元素调用方法或属性:
assert [1, 3, 5] == ['a', 'few', 'words']*.size()
class Person {
String name
int age
}
def persons = [new Person(name:'Hugo', age:17), new Person(name:'Sandra',age:19)]
assert [17, 19] == persons*.age
2.4.4 利用下标操作符进行截取操作
对列表、数组及 map,使用下标表达式,可以按索引访问。有趣的是,在这种情况下,字符串也可以被看成一种特殊的集合:
def text = 'nice cheese gromit!'
def x = text[2]
assert x == 'c'
assert x.class == String
def sub = text[5..10]
assert sub == 'cheese'
def list = [10, 11, 12, 13]
def answer = list[2,3]
assert answer == [12,13]
注意,也可以使用范围来抽取部分集合:
list = 100..200
sub = list[1, 3, 20..25, 33]
assert sub == [101, 103, 120, 121, 122, 123, 124, 125, 133]
下标操作符还可以用来更新一个已存在的(可改变)集合:
list = ['a','x','x','d']
list[1..2] = ['b','c']
assert list == ['a','b','c','d']
值得一提的是,允许出现负数,从而方便了从集合末尾起提取元素。
可以利用负数从列表、数组及字符串等结构的末尾起开始计数:
text = "nice cheese gromit!"
x = text[-1]
assert x == "!"
def name = text[-7..-2]
assert name == "gromit"
最后,如果你使用了反向的范围(起始索引大于末尾索引),所得结果也是反的。
text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"
2.5 增强的集合方法
除了列表、map
及范围之外,Groovy 还提供了大量的额外方法来实现过滤、收集、组织、计算等操作,这些方法都可以直接适用于集合,或者是更简单的实现了 iterable
接口的类。
关于更多细节,可以阅读 Groovy development kit API 文档,尤其是以下部分内容:
3. 实用工具
3.1 ConfigSlurper
ConfigSlurper
是一种能够读取 Groovy 脚本形式配置文件的工具类。就像与 Java 中 *.properties
文件那样,ConfigSlurper
可以使用点标记法。但除此之外,它还能支持闭包范围的配置值以及任意对象类型。
def config = new ConfigSlurper().parse('''
app.date = new Date() // 1⃣️
app.age = 42
app { // 2⃣️
name = "Test${42}"
}
''')
assert config.app.date instanceof Date
assert config.app.age == 42
assert config.app.name == 'Test42'
1⃣️ 使用点标记法
2⃣️ 使用闭包范围作为点标记法的替代方法
如上例所示,parse
方法可以用来获取 groovy.util.ConfigObject
实例。ConfigObject
是 java.util.Map
的一种专门实现,要么返回配置值,要么返回一个新的 ConfigObject
实例,但永远不会返回 null
值。
def config = new ConfigSlurper().parse('''
app.date = new Date()
app.age = 42
app.name = "Test${42}"
''')
assert config.test != null // 1⃣️
1⃣️ config.test
尚未被被指定为在调用时返回一个 ConfigObject
实例
当配置变量名包含点时,可以用单引号或双引号对该变量名实施转义处理:
def config = new ConfigSlurper().parse('''
app."person.age" = 42
''')
assert config.app."person.age" == 42
ConfigSlurper
还提供了对 environments
的支持。environments
方法可以用来移交一个本身含有一些部分的闭包实例。假如我们想要为开发环境创建一个特殊的配置值。在创建 ConfigSlurper
实例时,我们可以使用 ConfigSlurper(String)
构造函数来指定目标环境。
def config = new ConfigSlurper('development').parse('''
environments {
development {
app.port = 8080
}
test {
app.port = 8082
}
production {
app.port = 80
}
}
''')
assert config.app.port == 8080
ConfigSlurper
环境并不局限于任何特定的环境名称。它只依赖于其值受支持并被解析的 ConfigSlurper
客户端代码。
environments
方法是内建的,但 registerConditionalBlock
方法可以用来注册除environments
之外的其他方法名。
def slurper = new ConfigSlurper()
slurper.registerConditionalBlock('myProject', 'developers') // 1⃣️
def config = slurper.parse('''
sendMail = true
myProject {
developers {
sendMail = false
}
}
''')
assert !config.sendMail
1⃣️ 一旦新语句块注册成功,ConfigSlurper
就能解析它
为了能与 Java 整合,可以采用 toProperties
方法将 ConfigObject
对象转化为一种可能储存在*.properties
文本文件中的 java.util.Properties
对象。但要留意的是,在将配置值添加到新创建的 Properties
实例时,这些值会被转化 String
实例。
def config = new ConfigSlurper().parse('''
app.date = new Date()
app.age = 42
app {
name = "Test${42}"
}
''')
def properties = config.toProperties()
assert properties."app.date" instanceof String
assert properties."app.age" == '42'
assert properties."app.name" == 'Test42'
3.2 Expando
类
Expando
类可创建动态可扩展对象。虽然有 Expando
这样的名称,但它却不能使用 ExpandoMetaClass
。每一个 Expando
对象都表现为一个单独的、动态生成的实例,可以在运行时利用属性或方法来扩展。
def expando = new Expando()
expando.name = 'John'
assert expando.name == 'John'
动态属性注册了一个 Closure
代码块时,会出现一个特例。一经注册,即可调用,就好像是方法调用一般。
def expando = new Expando()
expando.toString = { -> 'John' }
expando.say = { String s -> "John says: ${s}" }
assert expando as String == 'John'
assert expando.say('Hi') == 'John says: Hi'
3.3 可观测的列表、映射与集
Groovy 提供了可观测的列表、映射与集。对于这其中每一种集合,在添加、删除或改变元素时,都会触发 java.beans.PropertyChangeEvent
事件。PropertyChangeEvent
并不仅仅是表示某个特定事件发生的标志,更重要的是,它记录着特定属性的名称以及该属性改变前后的值。
根据发生改变的类型,可观测集合可以发出更多的专门类型的 PropertyChangeEvent
事件。比如,可观测列表上添加元素,就会发出一个 ObservableList.ElementAddedEvent
事件。
def event 1⃣️
def listener = {
if (it instanceof ObservableList.ElementEvent) { 2⃣️
event = it
}
} as PropertyChangeListener
def observable = [1, 2, 3] as ObservableList 3⃣️
observable.addPropertyChangeListener(listener) 4⃣️
observable.add 42 5⃣️
assert event instanceof ObservableList.ElementAddedEvent
def elementAddedEvent = event as ObservableList.ElementAddedEvent
assert elementAddedEvent.changeType == ObservableList.ChangeType.ADDED
assert elementAddedEvent.index == 3
assert elementAddedEvent.oldValue == null
assert elementAddedEvent.newValue == 42
1⃣️ 声明一个捕获触发事件的 PropertyChangeEventListener
。
2⃣️ ObservableList.ElementEvent
与其子类型都跟该侦听器有关。
3⃣️ 注册侦听器。
4⃣️ 为指定列表创建一个 ObservableList
。
5⃣️ 触发 ObservableList.ElementAddedEvent
事件。
ObservableList.ElementClearedEvent
事件类型是比较有趣的,多个元素被删除时,比如在调用 clear()
时,它保存有将从列表中删除的元素。
def event
def listener = {
if (it instanceof ObservableList.ElementEvent) {
event = it
}
} as PropertyChangeListener
def observable = [1, 2, 3] as ObservableList
observable.addPropertyChangeListener(listener)
observable.clear()
assert event instanceof ObservableList.ElementClearedEvent
def elementClearedEvent = event as ObservableList.ElementClearedEvent
assert elementClearedEvent.values == [1, 2, 3]
assert observable.size() == 0
要想深入了解所有受支持的事件类型,读者需要查看相关 Java 文档或实际中使用的可观测集合的源代码。
与本节介绍的 ObservableList
相同,ObservableMap
与 ObservableSet
也具有相似的概念。