这是一篇译文,读者可前往 Groovy Getting Started - The Groovy Development Kit 阅读原文。
1 I/O
Groovy 为 I/O 提供了大量的便捷方法 。尽管你仍然可以在 Groovy 中使用标准的 Java 代码,但 Groovy 提供了更多方便的途径来处理文件、流等。
具体来说,你应该了解一下添加至如下类的方法:
接下来的内容将重点介绍如何使用上述便捷方法但并不会对所有的这些方法进行完整描述,具体请查阅 GDK API 。
1.1 读取文件
在第一个例子中,我们先来看看如何在 Groovy 中打印一个文本文件中的内容:
1 2 3 new File(baseDir, 'haiku.txt' ).eachLine { line -> println line }
eachLine
方法是由 Groovy 自动添加到 File
类中的新方法而且有很多的变体,例如如果你想要知道文件的行号,你可以使用如下这个变体:
1 2 3 new File(baseDir, 'haiku.txt' ).eachLine { line, nb -> println "Line $nb: $line" }
如果 eachLine
的方法体抛出了异常的话,eachLine
方法会确保所有相关资源都被正确地关闭。这一点对于所有由 Groovy 添加的 I/O 方法来说都是相同的。
例如在某些时候你更想使用 Reader
,但依然想利用上 Groovy 的自动资源管理。在下面的例子中,即使抛出了异常,所使用的 Reader
依然会被关闭:
1 2 3 4 5 6 7 8 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' ) } } }
如果你需要将一个文本文件的每一行内容放入到一个列表中,你可以这样做:
1 def list = new File(baseDir, 'haiku.txt' ).collect {it}
或者你也可以使用 as
操作符将文本文件每一行的内容放入到一个数组中:
1 def array = new File(baseDir, 'haiku.txt' ) as String[]
你有试过把文件的内容读入到一个 byte[]
中吗?这么做需要写多少代码呢?Groovy 则使得这么做变得十分简单:
1 byte [] contents = file.bytes
I/O 功能并不局限于文件读写。实际上,很大一部分操作依赖于输入输出流,因此 Groovy 为它们添加了大量的便捷方法,正如你在它们的文档 中看到的那样。
例如,你很容易就能够从一个 File
中获取一个 InputStream
:
1 2 3 def is = new File(baseDir,'haiku.txt' ).newInputStream()is.close()
然而,正如你所看到的那样,这样做会需要你自己关闭这个 InputStream
。实际上,在 Groovy 中使用 withInputStream
方法来处理资源管理是更好的选择:
1 2 3 new File(baseDir,'haiku.txt' ).withInputStream { stream -> }
1.2 写入文件
当然了,在某些情况下你可能会想要往文件中写入内容而不是读取内容。其中一种做法是使用 Writer
:
1 2 3 4 5 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!' }
但对于这么简单的功能,使用 <<
运算符也许也足够了:
1 2 3 new File(baseDir,'haiku.txt' ) << '''Into the ancient pond A frog jumps Water’s sound!'''
当然了,我们并不总是只需要处理文本内容,所以你也可以使用 Writer
或者像如下示例那样直接写入字节:
当然,你也可以直接处理输出流。例如,你可以像这个样子来创建一个能写入到文件的输出流:
1 2 3 def os = new File(baseDir,'data.bin' ).newOutputStream()os.close()
然而,正如你所见,这么做需要你自己关闭该输出流。同样,使用 withOutputStream
方法是更好的做法,因为它能处理抛出的异常并最终能在任何情况下关闭输出流:
1 2 3 new File(baseDir,'data.bin' ).withOutputStream { stream -> }
1.3 遍历文件树
在编写脚本的时候我们经常会需要遍历文件树来找到某些特定的文件并进行一些处理。Groovy 为此提供了多种不同的方法。例如你可以对文件夹中的所有文件执行指定的操作:
1 2 3 4 5 6 dir.eachFile { file -> println file.name } dir.eachFileMatch(~/.*\.txt/ ) { file -> println file.name }
<tr>
<td>1</td>
<td>对文件夹中的所有文件执行给定的闭包代码</td>
</tr>
<tr>
<td>2</td>
<td>对文件夹中所有匹配指定模式的文件执行给定的闭包代码</td>
</tr>
有时你还需要处理更深的文件层次,这时候你就需要使用 eachFileRecurse
了:
1 2 3 4 5 6 7 dir.eachFileRecurse { file -> println file.name } dir.eachFileRecurse(FileType.FILES) { file -> println file.name }
<tr>
<td>1</td>
<td>从该目录开始递归地查找所有文件或目录并执行指定的闭包代码</td>
</tr>
<tr>
<td>2</td>
<td>从该目录开始递归地查找所有文件并执行指定的闭包代码</td>
</tr>
对于更复杂的遍历操作你可以使用 traverse
方法,这需要你要返回特殊的标识位来指示如何进行遍历:
1 2 3 4 5 6 7 8 9 dir.traverse { file -> if (file.directory && file.name=='bin' ) { FileVisitResult.TERMINATE } else { println file.name FileVisitResult.CONTINUE } }
1
如果该文件为一个文件夹且名称为 `bin` 则停止遍历
2
否则打印文件的名称并继续遍历
1.4 数据与对象
在 Java 中,通过 java.io.DataOutputStream
和 java.io.DataInputStream
类来对数据进行序列化和反序列化并不少见,而 Groovy 则让这个过程变得更为简单。例如,你可以使用如下代码来将数据序列化到文件中并读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 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
接口,你还可以像如下代码那样使用 ObjectOutputStream
:
1 2 3 4 5 6 7 8 9 10 11 12 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
和输入输出流有多简便。然而,在诸如系统管理或者 DevOps 这样的领域中,我们则需要 Groovy 脚本能够与外部进程进行通信。
Groovy 提供了一种十分简单的方法来执行命令行进程,只要把命令行写作一个简单的 String
对象然后调用其 execute()
方法即可。例如,在一个 *nix 机器上(或者一个安装了合适的 *nix 命令行环境的 Windows 机器上),你可以这样做:
1 2 def process = "ls -l" .execute() println "Found text ${process.text}"
1
在一个外部进程中执行 `ls` 命令
2
消耗命令的输出并将其作为文本进行读取
execute()
方法会返回一个 java.lang.Process
对象,借由此我们可以对标准输入/标准输出/错误输出流进行处理,或者检查进程退出时的退出值。
例如,这里我们执行与上例相同的命令,但我们将逐行地处理其输出流:
1 2 3 4 def process = "ls -l" .execute() process.in .eachLine { line -> println line }
1
在一个外部进程中执行 `ls` 命令
2
对于该进程的输入流中的每一行内容
3
输出该内容
值得注意的是 in
代表的输入流对应着命令的标准输出,而你可以通过 out
代表的输出流向进程的标准输入写入数据。
注意,有不少命令实际上是 Shell 的内置功能,需要一些特殊的处理。所以如果你想要在一个 Windows 机器上列出一个文件夹内的所有文件,然后这样写的话:
1 2 def process = "dir" .execute()println "${process.text}"
你会得到一个 IOException
,内容如下:Cannot run program "dir": CreateProcess error=2, The system cannot find the file specified.
这是因为 dir
实际上是 Windows Shell(cmd.exe
)的一个内置功能,不能被当做一个单纯的可执行文件来运行。因此,你应该这样写:
1 2 def process = "cmd /c dir" .execute()println "${process.text}"
除此之外,由于这个功能实际上是通过 java.lang.Process
实现的,因此我们也应该考虑到这个类的一些不足之处。具体来说,它的 JavaDoc 是这么说的:
因为有些平台只为标准输入和输出流提供了很有限的缓存空间,写入输入流和读取输出流发生错误时可能会导致子进程发生阻塞,甚至发生死锁。
正是因为这个原因,Groovy 提供了一些额外的便捷方法来更好地处理外部进程的输入输出流。
通过如下代码你可以消耗掉进程的所有输出(包括错误流输出):
1 2 3 def p = "rm -f foo.tmp" .execute([], tmpDir)p.consumeProcessOutput() p.waitFor()
consumeProcessOutput
方法还包括其他一些变体可以利用 StringBuffer
、InputStream
、OutputStream
等,详见 java.lang.Process
的 GDK API 。
除此之外,还有一个 pipeTo
方法(对应于 |
操作符且可进行重载)可以将一个进程的输出流内容转移到另一个进程的输入流中。
如下为使用该方法的案例。
1 2 3 4 5 6 7 8 9 10 11 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 }
消耗错误流输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 为各种不同的集合类型提供了原生的语言支持,包括列表 、映射 和范围 。这些集合类大多数都基于 Java 原本的集合类型,同时加上了 GDK 特有的方法。
2.1 列表 2.1.1 列表字面量
你可以像如下代码那样创建列表。注意 []
是空列表表达式。
1 2 3 4 5 6 7 8 9 def list = [5 , 6 , 7 , 8 ]assert list.get(2 ) == 7 assert list[2 ] == 7 assert list instanceof java.util.Listdef emptyList = []assert emptyList.size() == 0 emptyList.add(5 ) assert emptyList.size() == 1
每一个列表表达式都会创建一个 java.util.List
实现类。
当然,列表也可以用于创建另一个列表:
1 2 3 4 5 6 7 8 9 def list1 = ['a' , 'b' , 'c' ]def list2 = new ArrayList<String>(list1)assert list2 == list1 def list3 = list1.clone()assert list3 == list1
列表实际上就是对象的有序集合:
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 def list = [5 , 6 , 7 , 8 ]assert list.size() == 4 assert list.getClass() == ArrayList assert list[2 ] == 7 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.5 f, 2.5 d, 'hello' , 7 g, 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 try { [1 , 2 , 3 , 4 , 5 ].get(-2 ) assert false } catch (e) { assert e instanceof ArrayIndexOutOfBoundsException }
2.1.2 将列表作为布尔表达式 列表可以被估作一个 boolean
值:
1 2 3 4 assert ![] assert [1 ] && ['a' ] && [0 ] && [0.0 ] && [false ] && [null ]
2.1.3 遍历列表 通常我们可以通过调用 each
或 eachWithIndex
方法来遍历列表的所有元素并给定处理元素的代码:
1 2 3 4 5 6 [1 , 2 , 3 ].each { println "Item: $it" } ['a' , 'b' , 'c' ].eachWithIndex { it, i -> println "$i: $it" }
除了遍历列表,有时我们还需要对一个列表的元素进行转换进而构建出另一个新的列表。这个操作,又被称为映射,在 Groovy 中可通过 collect
方法完成:
1 2 3 4 5 6 7 8 9 assert [1 , 2 , 3 ].collect { it * 2 } == [2 , 4 , 6 ]assert [1 , 2 , 3 ]*.multiply(2 ) == [1 , 2 , 3 ].collect { it.multiply(2 ) }def list = [0 ]assert [1 , 2 , 3 ].collect(list) { it * 2 } == [0 , 2 , 4 , 6 ]assert list == [0 , 2 , 4 , 6 ]
2.1.4 过滤和查找 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 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 assert ['a' , 'b' , 'c' , 'd' , 'c' ].lastIndexOf('c' ) == 4 assert [1 , 2 , 3 ].every { it < 5 } assert ![1 , 2 , 3 ].every { it < 3 }assert [1 , 2 , 3 ].any { it > 2 } assert ![1 , 2 , 3 ].any { it > 3 }assert [1 , 2 , 3 , 4 , 5 , 6 ].sum() == 21 assert ['a' , 'b' , 'c' , 'd' , 'e' ].sum { it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0 } == 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 还提供了在集合中查找最大值和最小值的方法:
1 2 3 4 5 6 7 8 9 10 11 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
来定义大小比较规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
2.1.5 添加和移除元素 我们可以使用 []
来创建一个新的空列表并用 <<
来向其中追加元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def list = []assert list.emptylist << 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 ]assert ([1 , 2 , 3 ] << 4 ) == ([1 , 2 , 3 ].leftShift(4 ))
除此之外很有很多种向列表中添加元素的方式:
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 assert [1 , 2 ] + 3 + [4 , 5 ] + 6 == [1 , 2 , 3 , 4 , 5 , 6 ]assert [1 , 2 ].plus(3 ).plus([4 , 5 ]).plus(6 ) == [1 , 2 , 3 , 4 , 5 , 6 ]def a = [1 , 2 , 3 ]a += 4 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 ) assert list == [1 , 3 , 2 ]list.addAll(2 , [5 , 4 ]) assert list == [1 , 3 , 5 , 4 , 2 ]list = ['a' , 'b' , 'z' , 'e' , 'u' , 'v' , 'g' ] list[8 ] = 'x' assert list == ['a' , 'b' , 'z' , 'e' , 'u' , 'v' , 'g' , null , 'x' ]
然而,值得注意的是,对列表使用 +
运算符并不会改变原列表。比起 <<
,它会产生出一个新的列表,很多时候这可能不是你想要的效果进而带来一些性能上的问题。
GDK 同样包含一些可以让你很方便地从列表中移除元素的方法:
1 2 3 4 5 6 7 8 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 assert list == [1 ,2 ,4 ,2 ,1 ]assert ( list -= [2 ,4 ] ) == [1 ,1 ]
同样,我们还可以通过给定元素的索引值来移除元素,而这种情况则会改变原本的列表:
1 2 3 def list = [1 ,2 ,3 ,4 ,5 ,6 ,2 ,2 ,1 ]assert list.removeAt(2 ) == 3 assert list == [1 ,2 ,4 ,5 ,6 ,2 ,2 ,1 ]
如果你只是想移除列表中第一个拥有给定值的元素而不是移除所有元素,你可以使用 remove
方法:
1 2 3 4 5 6 def list= ['a' ,'b' ,'c' ,'b' ,'b' ]assert list.remove('c' ) assert list.remove('b' ) assert ! list.remove('z' ) assert list == ['a' ,'b' ,'b' ]
通过 clear
方法可以移除列表中的所有元素:
1 2 3 def list= ['a' ,2 ,'c' ,4 ]list.clear() assert list == []
2.1.6 集合操作 GDK 还提供了可以更好地进行集合操作的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 assert 'a' in ['a' ,'b' ,'c' ] assert ['a' ,'b' ,'c' ].contains('a' ) assert [1 ,3 ,4 ].containsAll([1 ,4 ]) 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 ] )
2.1.7 排序
使用集合时通常需要对其进行排序。Groovy 同样提供了多种排序列表的方式,可以使用闭包或是提供 Comparator
,正如如下例子所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 } 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 ]
2.1.8 复制元素
GDK 还利用了运算符重载的功能为列表提供了复制元素的方法:
1 2 3 4 5 6 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' ]assert Collections.nCopies(2 , [1 , 2 ]) == [[1 , 2 ], [1 , 2 ]]
2.2 映射 2.2.1 映射字面量
在 Groovy 中,映射(又被称为联合数组)可使用映射字面量语法 [:]
创建:
1 2 3 4 5 6 7 8 9 10 11 12 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.Mapdef emptyMap = [:]assert emptyMap.size() == 0 emptyMap.put("foo" , 5 ) assert emptyMap.size() == 1 assert emptyMap.get("foo" ) == 5
映射的键默认为字符串:[a:1]
等价于 ['a':1]
。你有可能会没能意识到这种语句的含义,如果你定义了一个叫 a
的变量并且你想将它的值作为映射的键的话。如果你想要这样做的话,你应该像下面的例子那样为键加上括号来进行转义:
1 2 3 4 5 6 7 def a = 'Bob' def ages = [a: 43 ]assert ages['Bob' ] == null assert ages['a' ] == 43 ages = [(a): 43 ] assert ages['Bob' ] == 43
除了映射字面量,你还可以获取一个映射的拷贝:
1 2 3 4 5 6 7 8 9 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
正如上面的例子所示,所得的映射只是原映射的浅 拷贝。
2.2.2 映射属性访问语句
映射同样可以作为 Bean 使用,因此你也可以使用属性访问语句来访问映射,只要映射的键是字符串而且也是合法的 Groovy 标识符:
1 2 3 4 5 6 7 8 9 def map = [name: 'Gromit' , likes: 'cheese' , id: 1234 ]assert map.name == 'Gromit' assert map.id == 1234 def emptyMap = [:]assert emptyMap.size() == 0 emptyMap.foo = 5 assert emptyMap.size() == 1 assert emptyMap.foo == 5
注意,按这种规则的话,map.foo
会导致 Groovy 从映射 map
中查找 foo
。这意味着如果映射 map
不包含键 class
的话,map.class
会返回 null
。如果你只是想要获取映射的 Class
对象,你只能直接使用 getClass()
方法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ) 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 遍历映射 正如之前那样,GDK 同样为映射提供了 each
和 eachWithIndex
方法来进行遍历。值得注意的是通过映射字面量表达式创建的映射是有序的,也就是说如果你尝试遍历映射,映射中的键值对将总是以其被添加到映射中的顺序被遍历。
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 def map = [ Bob : 42 , Alice: 54 , Max : 33 ] map.each { entry -> println "Name: $entry.key Age: $entry.value" } map.eachWithIndex { entry, i -> println "$i - Name: $entry.key Age: $entry.value" } map.each { key, value -> println "Name: $key Age: $value" } map.eachWithIndex { key, value, i -> println "$i - Name: $key Age: $value" }
2.2.4 添加和删除元素
可以通过 put
方法、putAll
方法或下标运算符来将一个元素添加到映射中:
1 2 3 4 5 6 7 8 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' ]
调用 clear
方法可以移除映射中的所有元素:
1 2 3 4 def m = [1 :'a' , 2 :'b' ]assert m.get(1 ) == 'a' m.clear() assert m == [:]
由映射字面量语法产生的映射依赖于键的 equals
和 hashCode
方法,因此你不应使用那些 hashCode
会发生变化的对象作为键,否则你很有可能无法获取到其关联的值。
除此之外值得注意的是,你不应使用 GString
作为映射的键,因为 GString
的哈希码和内容与其相同的 String
的哈希码是不同的:
1 2 3 4 5 def key = 'some key' def map = [:]def gstringKey = "${key.toUpperCase()}" map.put(gstringKey,'value' ) assert map.get('SOME KEY' ) == null
2.1.5 键、值与键值对
我们可以在一个视图中读取映射的键、值和键值对:
1 2 3 4 5 6 7 8 9 10 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
通过该试图来修改映射(修改其键或值或键值对)都是不可取的,因为这样的操作是否能顺利执行直接取决于其背后被修改的映射的类型。具体来说,Groovy 所使用的来自 JDK 的集合类并不保证映射可以安全地通过其 keySet
、entrySet
或 values
视图进行修改。
2.1.6 过滤与查找
GDK 也为映射提供了与列表 类似的过滤、查找和收集方法:
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 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' }def ageOfBob = bob.value.agedef 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 ]assert people.every { id, person -> person.age > 18 } assert people.any { id, person -> person.age == 54 }
2.1.7 分组
我们可以通过给定一个条件来让列表中的元素各自分组形成一个列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 区间
你可以使用区间(Range)来创建一个由连续值组成的列表。区间可以被直接用作 List
因为 Range
扩展了 java.util.List
。
使用 ..
记号定义的区间是一个闭区间(也就是说该列表包含了起始值和终止值)。
使用 ..<
记号定义的区间则是一个半开区间:它包含起始值但不包含终止值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def range = 5. .8 assert range.size() == 4 assert range.get(2 ) == 7 assert range[2 ] == 7 assert range instanceof java.util.Listassert 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.Listassert range.contains(5 )assert !range.contains(8 )range = 1. .10 assert range.from == 1 assert range.to == 10
值得注意的是,int
类型区间的实现方式十分高效,实际上就是一个只包含了起始值和终止值的 Java 对象。
区间可以被用作任何实现了 java.lang.Comparable
接口用于进行大小比较,同时又有方法 next()
和 previous()
用于返回其上一个和下一个值的 Java 对象。例如,你可以创建一个由 String
元素组成的区间:
1 2 3 4 5 6 7 8 9 def range = 'a' ..'d' assert range.size() == 4 assert range.get(2 ) == 'c' assert range[2 ] == 'c' assert range instanceof java.util.Listassert range.contains('a' )assert range.contains('d' )assert !range.contains('e' )
你可以使用经典的 for
循环来迭代区间:
1 2 3 for (i in 1. .10 ) { println "Hello ${i}" }
但你也可以通过使用 each
方法来更 Groovy 地迭代区间:
1 2 3 (1. .10 ).each { i -> println "Hello ${i}" }
区间还可用于 switch
语句:
1 2 3 4 5 switch (years) { case 1. .10 : interestRate = 0.076 ; break ; case 11. .25 : interestRate = 0.052 ; break ; default: interestRate = 0.037 ; }
2.4 集合类的语法增强 2.4.1 GPath 支持 多亏了列表和映射都支持属性访问语法,在 Groovy 中我们可以使用语法糖来更好地应对嵌套集合,如下例所示:
1 2 3 4 5 6 7 8 9 def listOfMaps = [['a' : 11 , 'b' : 12 ], ['a' : 21 , 'b' : 22 ]]assert listOfMaps.a == [11 , 21 ] assert listOfMaps*.a == [11 , 21 ] listOfMaps = [['a' : 11 , 'b' : 12 ], ['a' : 21 , 'b' : 22 ], null ] assert listOfMaps*.a == [11 , 21 , null ] assert listOfMaps*.a == listOfMaps.collect { it?.a } assert listOfMaps.a == [11 ,21 ]
2.4.2 延伸运算符
延伸运算符可用于将一个集合“内联”到另一个集合之中。这个语法糖主要为了使我们不需要调用 putAll
方法并能写出更简短的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 assert [ 'z' : 900 , *: ['a' : 100 , 'b' : 200 ], 'a' : 300 ] == ['a' : 300 , 'b' : 200 , 'z' : 900 ] 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' ]f = { map -> map.c } assert f(*: ['a' : 10 , 'b' : 20 , 'c' : 30 ], 'e' : 50 ) == 30 f = { m, i, j, k -> [m, i, j, k] } 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 *.
运算符
星点运算符可用于调用集合中所有元素的某个方法或属性:
1 2 3 4 5 6 7 8 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 使用下标运算符进行分割
你可以使用下标运算符根据索引值来访问列表、元素和映射的元素。有趣的是在这种情况下,字符串也会被视作特殊的集合:
1 2 3 4 5 6 7 8 9 10 11 12 def text = 'nice cheese gromit!' def x = text[2 ]assert x == 'c' assert x.class == Stringdef sub = text[5. .10 ]assert sub == 'cheese' def list = [10 , 11 , 12 , 13 ]def answer = list[2 ,3 ]assert answer == [12 ,13 ]
值得注意的是你可以使用区间来获取集合中的一小部分:
1 2 3 list = 100. .200 sub = list[1 , 3 , 20. .25 , 33 ] assert sub == [101 , 103 , 120 , 121 , 122 , 123 , 124 , 125 , 133 ]
对于那些可变的集合,下标运算符可用于更新集合的值:
1 2 3 list = ['a' ,'x' ,'x' ,'d' ] list[1. .2 ] = ['b' ,'c' ] assert list == ['a' ,'b' ,'c' ,'d' ]
除此之外,你还可以使用负索引值来更好地从集合末尾开始提取元素:
1 2 3 4 5 6 text = "nice cheese gromit!" x = text[-1 ] assert x == "!" def name = text[-7. .-2 ]assert name == "gromit"
最后,如果你使用的是一个反向区间(起始值大于终止值),那么所得的结果也是反向的:
1 2 3 text = "nice cheese gromit!" name = text[3. .1 ] assert name == "eci"
2.5 新添加的集合方法
除了列表 、映射 和区间 以外,Groovy 还为其他集合或更普通的 Iterable
类提供了更多的用于过滤、收集、分组、计数等方法。
有关这方面的内容,我们希望你能仔细阅读 GDK 的 API 文档。具体来说:
在这里 可以找到 Iterable
的新方法
在这里 可以找到 Iterator
的新方法
在这里 可以找到 Collection
的新方法
在这里 可以找到 List
的新方法
在这里 可以找到 Map
的新方法
3 其他好用的功能 3.1 ConfigSlurper
ConfigSlurper
是可用于读取以 Groovy 脚本形式编写的配置文件的功能类。正如 Java 的 *.properties
文件那样,ConfigSlurper
也可以使用点号语法进行访问,除此之外它还能用闭包括号来给定配置值以及任意的对象类型:
1 2 3 4 5 6 7 8 9 10 11 def config = new ConfigSlurper().parse(''' app.date = new Date() // 注1 app.age = 42 app { // 注2 name = "Test${42}" } ''' )assert config.app.date instanceof Dateassert config.app.age == 42 assert config.app.name == 'Test42'
使用点号语法
使用闭包括号语法替代点号语法
正如我们在上一个例子中所见到的那样,parse
方法可用于获取一个 groovy.util.ConfigObject
实例。ConfigObject
是一种特殊的 java.util.Map
实现类,它要么返回具体的配置值要么返回一个新的 ConfigObject
,但绝不会返回 null
。
1 2 3 4 5 6 7 def config = new ConfigSlurper().parse(''' app.date = new Date() app.age = 42 app.name = "Test${42}" ''' )assert config.test != null
我们并未给出 `config.test`,但在被调用时仍然返回了一个 `ConfigObject`
如果点号本身需要作为配置变量的名称的话,可以使用单引号或双引号对其进行转义:
1 2 3 4 5 def config = new ConfigSlurper().parse(''' app."person.age" = 42 ''' )assert config.app."person.age" == 42
除此之外,ConfigSlurper
还支持不同的环境。environments
方法可被用于处理一个包含若干个配置小节的 Closure
实例。假设我们想要为开发环境创建一些特别的配置值。那么在创建 ConfigSlurper
实例时我们可以使用 ConfigSlurper(String)
构造器来给定目标环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
以外的方法名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def slurper = new ConfigSlurper()slurper.registerConditionalBlock('myProject' , 'developers' ) def config = slurper.parse(''' sendMail = true myProject { developers { sendMail = false } } ''' )assert !config.sendMail
在注册了新的代码块以后,ConfigSlurper
就能进行解析了
在与 Java 进行整合时,我们可以使用 toProperties
方法将 ConfigObject
转换成一个 java.util.Properties
,然后再将其存储至一个 *.properties
文本文件中。但要注意的是在转换成新的 Properties
实例的时候所有配置值都会被转换为 String
实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 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 Stringassert properties."app.age" == '42' assert properties."app.name" == 'Test42'
3.2 Expando
Expando
类可用于创建一个可动态扩展的对象。尽管它的名字看起来很像,但实际上它并没有利用 ExpandoMetaClass
来实现。每个 Expando
对象都代表一个独立的、可动态构造的实例,这些实例可在运行时用属性或方法进行扩展。
1 2 3 4 def expando = new Expando()expando.name = 'John' assert expando.name == 'John'
当将一个闭包代码块注册为动态属性时则比较特殊:在完成注册后可以像调用方法那样对其进行调用:
1 2 3 4 5 6 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
事件。值得注意的是一个 PropertiChangeEvent
并不只用于告诉监听器发生了特定的事件,它还包含了包括属性名以及属性修改前后的值等内容。
根据所发生的修改的类型,可观察的集合甚至可以一次触发多个不同类型的 PropertyChangeEvent
事件。例如,向一个可观察的列表中添加一个元素会触发 ObservableList.ElementAddedEvent
事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def event def listener = { if (it instanceof ObservableList.ElementEvent) { event = it } } as PropertyChangeListener def observable = [1 , 2 , 3 ] as ObservableList observable.addPropertyChangeListener(listener) observable.add 42 assert event instanceof ObservableList.ElementAddedEventdef elementAddedEvent = event as ObservableList.ElementAddedEventassert elementAddedEvent.changeType == ObservableList.ChangeType.ADDEDassert elementAddedEvent.index == 3 assert elementAddedEvent.oldValue == null assert elementAddedEvent.newValue == 42
声明一个 PropertyChangeEventListener
用于捕获触发的事件
ObservableList.ElementEvent
及其子类都会使该监听器起作用
注册监听器
用给定的列表创建一个 ObservableList
触发一个 ObservableList.ElementAddedEvent
事件
注意,添加元素实际上会触发两个事件。第一个事件即为 ObservableList.ElementAddedEvent
,而第二个实为一个 PropertyChangeEvent
,用于告诉监听器列表的大小属性发生了变化。
ObservableList.ElementClearedEvent
则是另一种比较有意思的事件。当列表中的复数元素被移除,例如被调用了 clear()
方法时,它会包含所有被从列表中移除的元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def eventdef listener = { if (it instanceof ObservableList.ElementEvent) { event = it } } as PropertyChangeListener def observable = [1 , 2 , 3 ] as ObservableListobservable.addPropertyChangeListener(listener) observable.clear() assert event instanceof ObservableList.ElementClearedEventdef elementClearedEvent = event as ObservableList.ElementClearedEventassert elementClearedEvent.values == [1 , 2 , 3 ]assert observable.size() == 0
为更好地了解所有支持的事件类型,读者可以参考所使用的可观察集合的 JavaDoc 文档或源代码。
ObservableMap
和 ObservableSet
同样包含了在这节中我们所看到的 ObservableList
所包含的功能。