目录
1. 处理 IO
在处理 IO 方面,Groovy 提供了很多帮助方法。尽管你也可以在 Groovy 中使用标准的 Java 代码来完成这些任务,但 Groovy 却提供了更加方便的方式来处理:文件、流、读取器...
下面这些由 Groovy 为 Java 类添加的方法是你特别需要关注的:
- java.io.File 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html
- java.io.InputStream 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html
- java.io.OutputStream 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.html
- java.io.Reader 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.html
- java.io.Writer 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.html
- java.nio.file.Path 类:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
本节下面的内容就聚焦于这些方法的使用,但这并不是所有方法的完整描述,如果想详细了解,请查看 GDK API
1.1 文件读取
作为第一个例子,我们来看下在 Groovy 中如何打印出一个文件中的所有行:
new File(baseDir, 'haiku.txt').eachLine { line ->
println line
}
eachLine 方法是由 Groovy 自动加入到 File 类中,它有很多变种,例如,如果你还想同时获取文件的行号的话,你可以使用下面这种形式的 eachLine:
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()
// do something ...
is.close()
但是,如你所见,这里需要你收到关闭这个输入流。在 Groovy 中,通常比较好的主意是使用 withInputStream 方法,这时 Groovy 就会负责为你管理好资源:
new File(baseDir,'haiku.txt').withInputStream { stream ->
// do something ...
}
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()
// do something ...
os.close()
这里同样需要你手动关闭这个输出流。同样,比较好的办法也是使用 withOutputStream,它会负责处理异常,保证任何情况下都会关闭这个流:
new File(baseDir,'data.bin').withOutputStream { stream ->
// do something ...
}
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.4 数据和对象
在 Java 中使用 java.io.DataOutputStream 和 java.io.DataInputStream 来分别进行数据的序列化和反序列化是很常见的。Groovy 使这些操作变得更加容易。例如,你可以用类似下面的这段代码来将数据序列化到文件,然后再把它反序列化:
boolean b = true
String message = 'Hello from Groovy'
// Serialize data into a file
file.withDataOutputStream { out ->
out.writeBoolean(b)
out.writeUTF(message)
}
// ...
// Then read it back
file.withDataInputStream { input ->
assert input.readBoolean() == b
assert input.readUTF() == message
}
类似地,如果你要序列化的数据实现了 Serializable 接口,你就可以使用对象输出流来处理该数据,演示如下:
Person p = new Person(name:'Bob', age:76)
// Serialize data into a file
file.withObjectOutputStream { out ->
out.writeObject(p)
}
// ...
// Then read it back
file.withObjectInputStream { input ->
def p2 = input.readObject()
assert p2.name == p.name
assert p2.age == p.age
}
1.5 执行外部程序
前几节展示了使用 Groovy 处理文件、流、读取器... 是多么便捷。然而,在系统管理或运维领域,我们经常需要和外部进程打交道。Groovy 提供了一种简单的方式来执行命令行程序。只需要把待执行的命令(String)写下来,然后调用它的 execute() 方法即可。例如在 *nix 系统(或安装了 *nix 命令的 windows 系统)上,你可以像下面这样执行外部命令:
def process = "ls -l".execute() //1
println "Found text ${process.text}" //2
//1: 在外部进程中执行 ls 命令
//2: 消耗命令的输出,并提取输出中的文本
String 类的 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 关联到 Groovy 程序里的一个输入流,在外部其实他是与外部进程的标准输出相连的。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 是 Windows shell (cmd.exe)中内置的命令,不是一个简单的可执行程序。因此你需要像下面这样来调用:
def process = "cmd /c dir".execute()
println "${process.text}"
此外,由于 execute() 执行外部程序在底层是基于 java.lang.Process 的,我们也必须明白这种方式的缺点,特别是 javaDoc 中对该类有如下描述:
因为一些底层平台只为标准输入和标准输出提供了有限大小的缓冲区,如果对子程序的标准输入写入不当或对其标准输出读取不当都可能导致子程序阻塞,甚至死锁。
因此,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 对各种集合类型(包括列表,映射,范围)提供了原生的支持。它们中大部分是基于 Java 的集合类,Groovy 为它们添加了额外的方法。
2.1 Lists
你可以像下面这样创建列表,请注意 [] 表示空列表:
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
每个列表表达式都创建了 java.util.List 的一个实现。
当然也可以用列表为 原料 来构造其他列表:
def list1 = ['a', 'b', 'c']
//construct a new list, seeded with the same items as in list1
def list2 = new ArrayList<String>(list1)
assert list2 == list1 // == checks that each corresponding element is the same
// clone() can also be called
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 // indexing starts at 0
assert list.getAt(2) == 7 // equivalent method to subscript operator []
assert list.get(2) == 7 // alternative method
list[2] = 9
assert list == [5, 6, 9, 8,] // 结尾允许有逗号
list.putAt(2, 10) // equivalent method to [] when value being changed
assert list == [5, 6, 10, 8]
assert list.set(2, 11) == 10 // alternative method that returns old value
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 // 负数下标从列表的结尾向前索引,-1 为最后一个元素
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 IndexOutOfBoundsException
}
2.1.2 列表作为布尔表达式
列表可以被求值成一个布尔值:
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"
}
除了迭代之外,我们经常需要对列表中的每个元素进行一些转换,然后得到一个新的列表。这种操作通常称为映射,在 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 开发包(GDK)包含很多集合方法,这些实用的方法有效增强了标准的集合类,下面例子展示了其中的部分方法:
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
} == 15 // 定制 sum 时使用的元素值
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']
// 可以为 sum 指定初始值
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
// we can also compare single characters, as anything comparable
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) // add 3 just before index 1
assert list == [1, 3, 2]
list.addAll(2, [5, 4]) //add [5,4] just before index 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']
值得注意的是列表上的 + 操作并不是就地改变被操作的列表。与 << 操作相比,+ 操作会创建一个新列表,可能带来性能问题,这通常并不是你想要的。
GDK 也提供了方法让你能够方便地从列表中移除指定的值:
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]
我们也可以将要移除的元素的下标传递给 remove 方法来实现移除元素,这是在列表上进行就地修改:
def list = ['a','b','c','d','e','f','b','b','a']
assert list.remove(2) == 'c' // 移除并返回第三个元素
assert list == ['a','b','d','e','f','b','b','a']
但是如果你只想移除列表中第一个与给定值相等的元素,而不是移除所有与给定值相等的元素,这可以把要移除的值作为参数传递给 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']
如你所见,有 2 个可用的 remove 方法。其中一个接受整形参数,并将其当作将要被移除元素的下标;另一个 remove 方法将会移除一个等于指定值的元素。那么如果我们有一个由整形元素组成的列表,这时要如何处理呢?这种情况下,你可能很乐意使用 removeAt 方法来通过下标的方式移除元素,使用 removeElement 方法来移除第一个等于入参给定值的元素:
def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3 // 这会移除下标为 2 的元素,并返回该元素
assert list == [1,2,4,5,6,2,2,1]
assert list.removeElement(2) // 移除第一个值为 2 的元素,并返回 true
assert list == [1,4,5,6,2,2,1]
assert ! list.removeElement(8) // 返回 false,因为 8 不在该列表中
assert list == [1,4,5,6,2,2,1]
assert list.removeAt(1) == 4 // 移除下标为 1 的元素,并返回该元素
assert list == [1,5,6,2,2,1]
当然,removeAt 和 removeElement 方法其实是可能操作任何类型的列表的。
此处,我们可以使用 clear 方法来移除列表中的所有元素:
def list= ['a',2,'c',4]
list.clear()
assert list == []
集合操作
GDK 同样包含了一些简化集合操作的方法:
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+ only
// 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]
复制操作
GDK 也利用了操作符重载的优势提供了复制列表元素的方法:
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 映射
2.2.1 Map 字面量
在 Groovy 中,可以使用映射字面量 [:] 语法来创建 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] // 默认情况下,a 被当作字符串 'a'
assert ages['Bob'] == null // 不存在键为 Bob 的元素
assert ages['a'] == 43 // 因为 a 是映射中包含的一个键
ages = [(a): 43] // 现在使用圆括号来转意 a, 这时它代表变量 a 的值所代表的键
assert ages['Bob'] == 43 // 这时就可以找到 Bob 对应的值了
除了使用 Map 字面量外,还可以使用 clone 来生成一份新的拷贝:
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 是原映射的一份浅拷贝。
2.2.2 Map 属性语法
映射的行为看起来类似于对象,所以也可以使用属性访问语法来获取或设置 Map 里面的元素,只要对应的键是字符串且是合法的 Groovy 标识符即可。
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit' // 可以用来代替 map.get('name')
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。这意味着在一个映射对象 map 上调用 map.class 将会返回 null,如果该 map 并不包含名为 class 的键。而如果你真的想获取的是 map 对象的 class 属性,这时你必须使用 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 不是一个合法的标识符所以直接使用,而不会被当作字符串 1
assert map.true == null // 此 map 中并不包含字符串 'true' 这个键
assert map.false == null // 此 map 中也不包含字符串 'false' 这个键
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z' // 此 map 中包含字符串 'null' 这个键,且值为 'z'
assert map.get(null) == 'x'
2.2.3 在映射上迭代
在 GDK 中很普遍的,在 Map 对象上进行迭代的惯用方式也是使用 each 和 eachWithIndex 方法。值得注意的是,使用 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"
}
// 此外也可以使用 key, 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 中添加元素,可以使用 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']
可以使用 clear 方法来移除 map 中的所有元素:
def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]
使用映射字面量语法创建的映射是基于对象的 equals 和 hashcode 方法的。这意味着,一定不要使用一个 hashcode 会随时间改变的对象作为键,否则将再也无法取回该键对应的值。
同样值得注意的是,不要使用 GString 作为映射的键,因为 GString 的 hashcode 和相等的 String 的 hashcode 是不一样的:
def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
键、值及条目
我们可以在视图中检查 Map 的键、值和条目:
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 进行安全的操作。
过滤和搜素
和列表操作类似,在 GDK 中也包含对映射的 过滤、搜素和收集方法:
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 方法来提取指定字段
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
}
分组
我们可以使用一些规则将一个列表分组为一个 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)
区间(Range)允许你创建一个由连续值组成的列表。它们可以被当作列表来使用,因为 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() 方法分别用于返回区间中下一个或前一个元素。例如,你可以创建一个由字符串元素组成的区间:
// 一个闭区间
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 增强的集合语法
2.4.1 GPath 支持
归功于列表和映射对象对属性访问语法的支持,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 定义中展开已有映射的语法
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 基于下标操作符的切片操作
你可以使用下标操作符来索引列表,数组和映射的元素。有趣的是,字符串也可被看作一个特殊的集合来使用下标操作:
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 增强的集合方法
除了对列表、映射、区间的增强外,Groovy 还提供了其他许多用来过滤、收集、分组、计数的实用方法,这些方法在集合类或更宽泛的来说,在可迭代对象上都是直接可用的。
我们特别推荐你阅读如下的 GDK API 文档:
3. 处理老式的 Date/Calendar 类型
groovy-dateutil 模块提供了许多扩展来和 Java 经典的 Date 和 Calendar 类协同工作。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-dateutil</artifactId>
<version>2.5.5</version>
</dependency>
你可以使用类似数组下标的语法来访问 Date 和 Calendar 类的属性,这里的下标其实是 Calendar 类里面定义的常量字段,请看下面的例子:
import static java.util.Calendar.* //1
def cal = Calendar.instance
cal[YEAR] = 2000 //2
cal[MONTH] = JANUARY //2
cal[DAY_OF_MONTH] = 1 //2
assert cal[DAY_OF_WEEK] == SATURDAY //3
//1: 导入常量
//2: 设置日历的年、月、日
//3: 访问日历的星期
如下例所示,Groovy 还支持 Date 和 Calendar 对象的算术运算和迭代:
def utc = TimeZone.getTimeZone('UTC')
Date date = Date.parse("yyyy-MM-dd HH:mm", "2010-05-23 09:01", utc)
def prev = date - 1
def next = date + 1
def diffInDays = next - prev
assert diffInDays == 2
int count = 0
prev.upto(next) { count++ }
assert count == 3
你可以把字符串解析为日期,也可以把日期输出为格式化的字符串:
def orig = '2000-01-01'
def newYear = Date.parse('yyyy-MM-dd', orig)
assert newYear[DAY_OF_WEEK] == SATURDAY
assert newYear.format('yyyy-MM-dd') == orig
assert newYear.format('dd/MM/yyyy') == '01/01/2000'
你也可以基于一个已有的 Date 或 Calendar 对象来创建一个新的:
def newYear = Date.parse('yyyy-MM-dd', '2000-01-01')
def newYearsEve = newYear.copyWith(
year: 1999,
month: DECEMBER,
dayOfMonth: 31
)
assert newYearsEve[DAY_OF_WEEK] == FRIDAY
4. 处理 Date/Time 类型
groovy-datetime 模块里提供了大量的扩展来和 Java 8 中引入的 Date/Time API 协同工作。本文中我们把这个 API 定义的数据类型称作 JSR 310 类型。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-datetime</artifactId>
<version>2.5.5</version>
</dependency>
4.1 格式化和解析
处理 Date/Time 类型时的一个常用操作是在它们和字符串之间做转换,把它们转换为字符串(格式化),把字符串转换为 Date/Time 类型(解析):
方法 | 格式化不同日期时间类型所使用的格式化类 | 举例 |
---|---|---|
| 使用 |
|
使用 |
| |
使用 |
| |
| 使用 |
|
使用 |
| |
使用 |
| |
| 使用 |
|
使用 |
| |
使用 |
| |
| 使用 |
|
使用 |
| |
使用 |
| |
|
|
|
在解析方面,Groovy 在许多 JSR 310 类型上添加了一个静态的 parse 方法。该方法接受两个参数:要解析的值和解析时使用的模式。模式是由 java.time.format.DateTimeFormatter API 定义的。举个例子如下:
def date = LocalDate.parse('Jun 3, 04', 'MMM d, yy')
assert date == LocalDate.of(2004, Month.JUNE, 3)
def time = LocalTime.parse('4:45', 'H:mm')
assert time == LocalTime.of(4, 45, 0)
def offsetTime = OffsetTime.parse('09:47:51-1234', 'HH:mm:ssZ')
assert offsetTime == OffsetTime.of(9, 47, 51, 0, ZoneOffset.ofHoursMinutes(-12, -34))
def dateTime = ZonedDateTime.parse('2017/07/11 9:47PM Pacific Standard Time', 'yyyy/MM/dd h:mma zzzz')
assert dateTime == ZonedDateTime.of(
LocalDate.of(2017, 7, 11),
LocalTime.of(21, 47, 0),
ZoneId.of('America/Los_Angeles')
)
请注意这些 parse 方法中参数的顺序和 Groovy 向 java.util.Date 类中添加的静态 parse 方法中的顺序是不一样的。这样做是为了和 Date/Time API 中现有的 parse 方法保持一致。
4.2 操作日期/时间
4.2.1 加减操作
Temporal 类型有 plus 和 minus 方法,用来加上或减去一个由参数指定的 java.time.temporal.TemporalAmount 值。因为 Groovy 分别将 + 和 - 操作符映射到了接受单个参数的 plus 和 minus 方法,所以可以使用运算符来书写更加自然的加减表达式语法。
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def nextAprilFools = aprilFools + Period.ofDays(365) // 加上 365 天
assert nextAprilFools.year == 2019
def idesOfMarch = aprilFools - Period.ofDays(17) // 减去 17 天
assert idesOfMarch.dayOfMonth == 15
assert idesOfMarch.month == Month.MARCH
Groovy 还提供了接受整形参数的 plus 和 minus 方法,可以使上面的写法变得更简单:
def nextAprilFools = aprilFools + 365 // 加上 365 天
def idesOfMarch = aprilFools - 17 // 减去 17 天
这些接受整形参数的方法中,参数的单位是由被操作的具体 JSR 310 类型来决定的。如上面的例子所示,在操作 ChronoLocalDate 类型的变量(如 LocalDate)时,使用的单位是:天。当操作 Year 和 YearMonth 时,单位分别为: 年 years 和月 months 。对所有其他的类型(如 LocalTime),参数的单位都为:秒 seconds,例如:
def mars = LocalTime.of(12, 34, 56) // 下午 12:34:56
def thirtySecondsToMars = mars - 30 // 回退 30 秒
assert thirtySecondsToMars.second == 26
4.2.2 乘除操作
乘法操作符 * 可以用来在 Period 和 Duration 对象上乘以一个整形值。除法操作符 / 可以用来在 Duration 对象上除以一个整形值。
def period = Period.ofMonths(1) * 2 // 一个月的时间乘以 2
assert period.months == 2
def duration = Duration.ofSeconds(10) / 5 // 10s 的间隔除以 5
assert duration.seconds == 2
4.2.3 自增自减操作
自增 ++ 和自减 -- 操作符可以分别为 date/time 类型的值增加和减少一个时间单位。因为 JSR 310 类型是不可变的,所以该操作其实是使用自增或自减后的值创建一个新的对象,并将其引用赋值给原变量。
def year = Year.of(2000)
--year // 减少 1 年
assert year.value == 1999
def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
offsetTime++ // 增加 1 秒
assert offsetTime.second == 1
4.2.4 符号取反
Duration 和 Period 类型代表了一段正或负的时间长度。可以使用一元操作符 - 对其进行取反:
def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15
4.3 与 date/time 值交互
4.3.1 属性访问语法