Groovy 开发工具包
1. IO 操作
Groovy 提供了许多用于 IO 操作的辅助方法。虽然您可以在 Groovy 中使用标准的 Java 代码来处理这些,但 Groovy 提供了更方便的方式来处理文件、流、读取器等。
特别是,您应该查看以下类中添加的方法:
-
java.io.File
类:https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/File.html -
java.io.InputStream
类:https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/InputStream.html -
java.io.OutputStream
类:https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/OutputStream.html -
java.io.Reader
类:https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/Reader.html -
java.io.Writer
类:https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/Writer.html -
java.nio.file.Path
类:https://docs.groovy-lang.cn/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
类的方法,它有许多变体,例如,如果您需要知道行号,可以使用此变体
new File(baseDir, 'haiku.txt').eachLine { line, nb ->
println "Line $nb: $line"
}
如果出于任何原因在 eachLine
主体中抛出异常,该方法会确保资源正确关闭。Groovy 添加的所有 I/O 资源方法都是如此。
例如,在某些情况下,您更喜欢使用 Reader
,但仍然受益于 Groovy 的自动资源管理。在下一个示例中,即使发生异常,读取器也将被关闭
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
惯用法,它将为您处理此问题
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 中处理文件、读取器或流是多么容易。然而,在系统管理或 DevOps 等领域,通常需要与外部进程进行通信。
Groovy 提供了一种执行命令行进程的简单方法。只需将命令行写成字符串并调用 execute()
方法。例如,在 *nix 机器(或安装了适当 *nix 命令的 Windows 机器)上,您可以执行此操作
def process = "ls -l".execute() (1)
println "Found text ${process.text}" (2)
1 | 在外部进程中执行 ls 命令 |
2 | 使用命令的输出并检索文本 |
execute()
方法返回一个 java.lang.Process
实例,该实例随后将允许处理输入/输出/错误流并检查进程的退出值等。
例如,这是与上面相同的命令,但我们现在将一次处理一行结果流
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
,提示 无法运行程序 "dir":CreateProcess error=2,系统找不到指定的文件。
这是因为 dir
是 Windows shell (cmd.exe
) 的内置命令,不能作为简单的可执行文件运行。相反,您需要写入
def process = "cmd /c dir".execute()
println "${process.text}"
此外,由于此功能目前在后台使用 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. 列表
2.1.1. 列表字面量
您可以按如下方式创建列表。请注意,[]
是空列表表达式。
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 // the specific kind of list being used
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,] // trailing comma OK
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]
//objects can be of different types; duplicates allowed
assert [1, 2, 3, 4, 5][-1] == 5 // use negative indices to count from the end
assert [1, 2, 3, 4, 5][-2] == 4
assert [1, 2, 3, 4, 5].getAt(-2) == 4 // getAt() available with negative index...
try {
[1, 2, 3, 4, 5].get(-2) // but negative index not allowed with get()
assert false
} catch (e) {
assert e instanceof IndexOutOfBoundsException
}
2.1.2. 列表作为布尔表达式
列表可以评估为 boolean
值
assert ![] // an empty list evaluates as false
//all other lists, irrespective of contents, evaluate as true
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]
2.1.3. 列表迭代
迭代列表元素通常通过调用 each
和 eachWithIndex
方法来完成,这些方法对列表中的每个项执行代码
[1, 2, 3].each {
println "Item: $it" // `it` is an implicit parameter corresponding to the current element
}
['a', 'b', 'c'].eachWithIndex { it, i -> // `it` is the current element, while `i` is the index
println "$i: $it"
}
除了迭代之外,通过将每个元素转换为其他东西来创建新列表也常常很有用。这种操作通常称为映射,在 Groovy 中通过 collect
方法完成
assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]
// shortcut syntax instead of collect
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }
def list = [0]
// it is possible to give `collect` the list which collects the elements
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]
2.1.4. 列表操作
过滤和搜索
Groovy 开发工具包包含许多集合方法,这些方法通过实用的方法增强了标准集合,其中一些示例如下
assert [1, 2, 3].find { it > 1 } == 2 // find 1st element matching criteria
assert [1, 2, 3].findAll { it > 1 } == [2, 3] // find all elements matching critieria
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf { // find index of 1st element matching criteria
it in ['c', 'e', 'g']
} == 2
assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2 // index returned
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // index -1 means value not in list
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4
assert [1, 2, 3].every { it < 5 } // returns true if all elements match the predicate
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 } // returns true if any element matches the predicate
assert ![1, 2, 3].any { it > 3 }
assert [1, 2, 3, 4, 5, 6].sum() == 21 // sum anything with a plus() method
assert ['a', 'b', 'c', 'd', 'e'].sum {
it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
// custom value to use in 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']
// an initial value can be provided
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006
assert [1, 2, 3].join('-') == '1-2-3' // String joining
assert [1, 2, 3].inject('counting: ') {
str, item -> str + item // reduce operation
} == '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'
// we can use a closure to specify the sorting behaviour
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']]
//first item in chain of << is target list
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]
//using leftShift is equivalent to using <<
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))
我们可以通过多种方式向列表添加元素
assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// equivalent to calling the `plus` method
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]
def a = [1, 2, 3]
a += 4 // creates a new list and assigns it to `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' // the [] operator is growing the list as needed
// nulls inserted if required
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']
然而,重要的是,列表上的 +
运算符是不可变的。与 <<
相比,它将创建一个新列表,这通常不是您想要的,并且可能导致性能问题。
Groovy 开发工具包还包含允许您按值轻松从列表中删除元素的方法
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 // creates a new list by removing `3` from the original one
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' // remove the third element, and return it
assert list == ['a','b','d','e','f','b','b','a']
如果您只想删除列表中具有相同值的第一个元素,而不是删除所有元素,则可以调用 remove
方法并传递该值
def list= ['a','b','c','b','b']
assert list.remove('c') // remove 'c', and return true because element removed
assert list.remove('b') // remove first 'b', and return true because element removed
assert ! list.remove('z') // return false because no elements removed
assert list == ['a','b','b']
如您所见,有两个 remove
方法可用。一个接受一个整数并按其索引删除元素,另一个将删除与传递值匹配的第一个元素。那么,当我们有一个整数列表时,我们应该怎么做呢?在这种情况下,您可能希望使用 removeAt
按其索引删除元素,并使用 removeElement
删除与值匹配的第一个元素。
def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3 // this removes the element at index 2, and returns it
assert list == [1,2,4,5,6,2,2,1]
assert list.removeElement(2) // remove first 2 and return true
assert list == [1,4,5,6,2,2,1]
assert ! list.removeElement(8) // return false because 8 is not in the list
assert list == [1,4,5,6,2,2,1]
assert list.removeAt(1) == 4 // remove element at index 1, and return it
assert list == [1,5,6,2,2,1]
当然,removeAt
和 removeElement
将适用于任何类型的列表。
此外,可以通过调用 clear
方法来删除列表中的所有元素
def list= ['a',2,'c',4]
list.clear()
assert list == []
集合操作
Groovy 开发工具包还包括使集合推理变得容易的方法
assert 'a' in ['a','b','c'] // returns true if an element belongs to the list
assert ['a','b','c'].contains('a') // equivalent to the `contains` method in Java
assert [1,3,4].containsAll([1,4]) // `containsAll` will check that all elements are found
assert [1,2,3,3,3,3,4,5].count(3) == 4 // count the number of elements which have some value
assert [1,2,3,3,3,3,4,5].count {
it%2==0 // count the number of elements which match the predicate
} == 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]
复制元素
Groovy 开发工具包也利用运算符重载提供了允许复制列表元素的方法
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']
// nCopies from the JDK has different semantics than multiply for lists
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] //not [1,2,1,2]
2.2. 映射
2.2.1. 映射字面量
在 Groovy 中,可以使用映射字面量语法创建映射(也称为关联数组):[:]
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
默认情况下,映射键是字符串:[a:1]
等同于 ['a':1]
。如果您定义了一个名为 a
的变量,并且您希望 a
的值作为映射中的键,这可能会令人困惑。如果是这种情况,则您必须通过添加括号来转义键,如下例所示
def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null // `Bob` is not found
assert ages['a'] == 43 // because `a` is a literal!
ages = [(a): 43] // now we escape `a` by using parenthesis
assert ages['Bob'] == 43 // and the value is found!
除了映射字面量,还可以克隆映射以获取映射的新副本
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 标识符字符串,您就可以使用属性表示法在 Map
中获取/设置项
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit' // can be used instead of 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
始终会在映射中查找键 foo
。这意味着在不包含 class
键的映射上,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 // this is probably what you want
map = [1 : 'a',
(true) : 'p',
(false): 'q',
(null) : 'x',
'null' : 'z']
assert map.containsKey(1) // 1 is not an identifier so used as is
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. 映射迭代
像往常一样,在 Groovy 开发工具包中,对映射的惯用迭代使用了 each
和 eachWithIndex
方法。值得注意的是,使用映射字面量表示法创建的映射是有序的,也就是说,如果您迭代映射条目,则保证条目将按照它们添加到映射中的相同顺序返回。
def map = [
Bob : 42,
Alice: 54,
Max : 33
]
// `entry` is a map entry
map.each { entry ->
println "Name: $entry.key Age: $entry.value"
}
// `entry` is a map entry, `i` the index in the map
map.eachWithIndex { entry, i ->
println "$i - Name: $entry.key Age: $entry.value"
}
// Alternatively you can use key and value directly
map.each { key, value ->
println "Name: $key Age: $value"
}
// Key, value and i as the index in the map
map.eachWithIndex { key, value, i ->
println "$i - Name: $key Age: $value"
}
2.2.4. 映射操作
添加或删除元素
向映射添加元素可以使用 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
方法来删除映射的所有元素
def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]
使用映射字面量语法生成的映射使用对象的 equals
和 hashcode
方法。这意味着您绝不能使用哈希码随时间变化的对象,否则您将无法取回相关值。
值得注意的是,您绝不能使用 GString
作为映射的键,因为 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
强烈不鼓励修改视图返回的值(无论是映射条目、键还是值),因为操作的成功直接取决于被操作的映射类型。特别是,Groovy 依赖于 JDK 中的集合,这些集合通常不保证可以通过 keySet
、entrySet
或 values
安全地操作集合。
过滤和搜索
Groovy 开发工具包包含与列表类似的过滤、搜索和收集方法
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' } // find a single entry
def females = people.findAll { it.value.gender == 'F' }
// both return entries, but you can use collect to retrieve the ages for example
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
it.value.age
}
assert ageOfBob == 32
assert agesOfFemales == [21,54]
// but you could also use a key/pair value as the parameters of the closures
def agesOfMales = people.findAll { id, person ->
person.gender == 'M'
}.collect { id, person ->
person.age
}
assert agesOfMales == [32, 36]
// `every` returns true if all entries match the predicate
assert people.every { id, person ->
person.age > 18
}
// `any` returns true if any entry matches the predicate
assert people.any { id, person ->
person.age == 54
}
分组
我们可以使用一些标准将列表分组到映射中
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 扩展了 java.util.List,因此它们可以用作 List
。
用 ..
符号定义的范围是包含的(即列表包含起始值和结束值)。
用 ..<
符号定义的范围是半开的,它们包含第一个值但不包含最后一个值。
用 <..
符号定义的范围也是半开的,它们包含最后一个值但不包含第一个值。
用 <..<
符号定义的范围是全开的,它们既不包含第一个值也不包含最后一个值。
// an inclusive range
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)
// lets use a half-open range
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)
//get the end points of the range without using indexes
range = 1..10
assert range.from == 1
assert range.to == 10
请注意,整数范围的实现效率很高,创建了一个包含起始值和结束值的轻量级 Java 对象。
范围可以用于任何实现了 java.lang.Comparable 进行比较且具有 next()
和 previous()
方法以返回范围内下一个/上一个项的 Java 对象。例如,您可以创建 String
元素的范围
// an inclusive range
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 notation
assert listOfMaps*.a == [11, 21] //spread dot notation
listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // caters for null values
assert listOfMaps*.a == listOfMaps.collect { it?.a } //equivalent notation
// But this will only collect non-null values
assert listOfMaps.a == [11,21]
2.4.2. 展开运算符
展开运算符可用于将一个集合“内联”到另一个集合中。这是一种语法糖,通常可以避免调用 putAll
并促进单行代码的实现
assert [ 'z': 900,
*: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
//spread map notation in map definition
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']
//spread map notation in function arguments
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30
f = { m, i, j, k -> [m, i, j, k] }
//using spread map notation with mixed unnamed and named arguments
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 开发工具包 API 文档,特别是
3. 数组操作
Groovy 提供了基于 Java 数组的数组支持,并提供了Groovy 开发工具包中的几个扩展。总体意图是,无论您使用数组还是集合,处理聚合的代码都保持不变。
3.1. 数组
3.1.1. 数组字面量
您可以按如下方式创建数组。请注意,当给定显式数组类型时,[]
也用作空数组表达式。
Integer[] nums = [5, 6, 7, 8]
assert nums[1] == 6
assert nums.getAt(2) == 7 // alternative syntax
assert nums[-1] == 8 // negative indices
assert nums instanceof Integer[]
int[] primes = [2, 3, 5, 7] // primitives
assert primes instanceof int[]
def evens = new int[]{2, 4, 6} // alt syntax 1
assert evens instanceof int[]
def odds = [1, 3, 5] as int[] // alt syntax 2
assert odds instanceof int[]
// empty array examples
Integer[] emptyNums = []
assert emptyNums instanceof Integer[] && emptyNums.size() == 0
def emptyStrings = new String[]{} // alternative syntax 1
assert emptyStrings instanceof String[] && emptyStrings.size() == 0
var emptyObjects = new Object[0] // alternative syntax 2
assert emptyObjects instanceof Object[] && emptyObjects.size() == 0
3.1.2. 列表迭代
迭代列表元素通常通过调用 each
和 eachWithIndex
方法来完成,这些方法对列表中的每个项执行代码
String[] vowels = ['a', 'e', 'i', 'o', 'u']
var result = ''
vowels.each {
result += it
}
assert result == 'aeiou'
result = ''
vowels.eachWithIndex { v, i ->
result += v * i // index starts from 0
}
assert result == 'eiiooouuuu'
3.1.3. 其他有用方法
还有许多其他用于处理数组的 GDK 方法。请仔细阅读文档。对于集合,有一些修改原始集合的变异方法,还有一些生成新集合而不修改原始集合的方法。由于数组是固定大小的,我们不期望有改变数组大小的变异方法。相反,这些方法通常返回集合。以下是一些有趣的数组 GDK 方法
int[] nums = [1, 2, 3]
def doubled = nums.collect { it * 2 }
assert doubled == [2, 4, 6] && doubled instanceof List
def tripled = nums*.multiply(3)
assert tripled == [3, 6, 9] && doubled instanceof List
assert nums.any{ it > 2 }
assert nums.every{ it < 4 }
assert nums.average() == 2
assert nums.min() == 1
assert nums.max() == 3
assert nums.sum() == 6
assert nums.indices == [0, 1, 2]
assert nums.swap(0, 2) == [3, 2, 1] as int[]
4. 旧版 Date/Calendar 类型操作
groovy-dateutil
模块支持许多用于处理 Java 经典 Date
和 Calendar
类的扩展。
您可以使用常量字段号从 Calendar
类中以正常的数组索引表示法访问 Date
或 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'
您还可以根据现有日期或日历创建新的日期或日历
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
5. 日期/时间类型操作
groovy-datetime
模块支持 Java 8 中引入的 日期/时间 API 的众多扩展。本文档将此 API 定义的数据类型称为“JSR 310 类型”。
5.1. 格式化和解析
处理日期/时间类型时的一个常见用例是将它们转换为字符串(格式化)和从字符串转换(解析)。Groovy 提供了这些额外的格式化方法
方法 | 描述 | 示例 |
---|---|---|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
|
对于解析,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
方法不同。这样做是为了与日期/时间 API 现有 parse
方法保持一致。
5.2. 日期/时间操作
5.2.1. 加法和减法
Temporal
类型具有 plus
和 minus
方法,用于添加或减去提供的 java.time.temporal.TemporalAmount
参数。因为 Groovy 将 +
和 -
运算符映射到这些名称的单参数方法,所以可以使用更自然的表达式语法进行加法和减法。
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def nextAprilFools = aprilFools + Period.ofDays(365) // add 365 days
assert nextAprilFools.year == 2019
def idesOfMarch = aprilFools - Period.ofDays(17) // subtract 17 days
assert idesOfMarch.dayOfMonth == 15
assert idesOfMarch.month == Month.MARCH
Groovy 提供了额外的 plus
和 minus
方法,它们接受一个整数参数,从而可以更简洁地重写上述内容
def nextAprilFools = aprilFools + 365 // add 365 days
def idesOfMarch = aprilFools - 17 // subtract 17 days
这些整数的单位取决于 JSR 310 类型操作数。如上所示,与 LocalDate
等 ChronoLocalDate
类型一起使用的整数的单位是天。与 Year
和 YearMonth
一起使用的整数的单位分别是年和月。所有其他类型的单位都是秒,例如 LocalTime
def mars = LocalTime.of(12, 34, 56) // 12:34:56 pm
def thirtySecondsToMars = mars - 30 // go back 30 seconds
assert thirtySecondsToMars.second == 26
5.2.2. 乘法和除法
*
运算符可用于将 Period
和 Duration
实例乘以整数值;/
运算符可用于将 Duration
实例除以整数值。
def period = Period.ofMonths(1) * 2 // a 1-month period times 2
assert period.months == 2
def duration = Duration.ofSeconds(10) / 5// a 10-second duration divided by 5
assert duration.seconds == 2
5.2.3. 递增和递减
++
和 --
运算符可用于将日期/时间值递增和递减一个单位。由于 JSR 310 类型是不可变的,因此该操作将创建一个具有递增/递减值的新实例并将其重新分配给引用。
def year = Year.of(2000)
--year // decrement by one year
assert year.value == 1999
def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
offsetTime++ // increment by one second
assert offsetTime.second == 1
5.2.4. 取反
Duration
和 Period
类型表示时间段的正负长度。它们可以使用一元 -
运算符取反。
def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15
5.3. 日期/时间值交互
5.3.1. 属性表示法
TemporalAccessor
类型的 getLong(TemporalField)
方法(例如 LocalDate
, LocalTime
, ZonedDateTime
等)和 TemporalAmount
类型的 get(TemporalUnit)
方法(即 Period
和 Duration
)可以通过 Groovy 的属性表示法调用。例如
def date = LocalDate.of(2018, Month.MARCH, 12)
assert date[ChronoField.YEAR] == 2018
assert date[ChronoField.MONTH_OF_YEAR] == Month.MARCH.value
assert date[ChronoField.DAY_OF_MONTH] == 12
assert date[ChronoField.DAY_OF_WEEK] == DayOfWeek.MONDAY.value
def period = Period.ofYears(2).withMonths(4).withDays(6)
assert period[ChronoUnit.YEARS] == 2
assert period[ChronoUnit.MONTHS] == 4
assert period[ChronoUnit.DAYS] == 6
5.3.2. 范围、upto
和 downto
JSR 310 类型可以与范围运算符一起使用。以下示例在今天和六天后的 LocalDate
之间进行迭代,打印出每次迭代的星期几。由于两个范围边界都是包含的,因此这会打印出一周中的所有七天。
def start = LocalDate.now()
def end = start + 6 // 6 days later
(start..end).each { date ->
println date.dayOfWeek
}
upto
方法将实现与上述示例中的范围相同的功能。upto
方法从较早的 start
值(包含)迭代到较晚的 end
值(也包含),在每次迭代中调用带有递增 next
值的闭包。
def start = LocalDate.now()
def end = start + 6 // 6 days later
start.upto(end) { next ->
println next.dayOfWeek
}
downto
方法则沿相反方向迭代,从较晚的 start
值到较早的 end
值。
upto
、downto
和范围的迭代单位与加减法的单位相同:LocalDate
一次迭代一天,YearMonth
迭代一个月,Year
迭代一年,其余所有迭代一秒。两种方法都支持可选的 TemporalUnit
参数来更改迭代单位。
考虑以下示例,其中 2018 年 3 月 1 日使用以月为单位的迭代方式迭代到 2018 年 3 月 2 日。
def start = LocalDate.of(2018, Month.MARCH, 1)
def end = start + 1 // 1 day later
int iterationCount = 0
start.upto(end, ChronoUnit.MONTHS) { next ->
println next
++iterationCount
}
assert iterationCount == 1
由于 start
日期是包含的,因此闭包以 3 月 1 日的 next
日期值调用。然后 upto
方法将日期递增一个月,得到 4 月 1 日。因为此日期在指定的 3 月 2 日 end
日期之后,所以迭代立即停止,只调用了一次闭包。此行为与 downto
方法相同,只是迭代将在 next
的值早于目标 end
日期时立即停止。
简而言之,当使用 upto
或 downto
方法进行自定义迭代单位的迭代时,当前迭代值永远不会超过结束值。
5.3.3. 组合日期/时间值
左移运算符(<<
)可用于将两个 JSR 310 类型组合成一个聚合类型。例如,可以将 LocalDate
左移到 LocalTime
中,以生成复合 LocalDateTime
实例。
MonthDay monthDay = Month.JUNE << 3 // June 3rd
LocalDate date = monthDay << Year.of(2015) // 3-Jun-2015
LocalDateTime dateTime = date << LocalTime.NOON // 3-Jun-2015 @ 12pm
OffsetDateTime offsetDateTime = dateTime << ZoneOffset.ofHours(-5) // 3-Jun-2015 @ 12pm UTC-5
左移运算符是自反的;操作数的顺序无关紧要。
def year = Year.of(2000)
def month = Month.DECEMBER
YearMonth a = year << month
YearMonth b = month << year
assert a == b
5.3.4. 创建周期和持续时间
右移运算符(>>
)生成表示操作数之间周期或持续时间的值。对于 ChronoLocalDate
、YearMonth
和 Year
,该运算符生成 Period
实例
def newYears = LocalDate.of(2018, Month.JANUARY, 1)
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def period = newYears >> aprilFools
assert period instanceof Period
assert period.months == 3
该运算符为时间感知的 JSR 类型生成 Duration
def duration = LocalTime.NOON >> (LocalTime.NOON + 30)
assert duration instanceof Duration
assert duration.seconds == 30
如果运算符左侧的值早于右侧的值,则结果为正。如果左侧晚于右侧,则结果为负
def decade = Year.of(2010) >> Year.of(2000)
assert decade.years == -10
5.4. 旧版类型和 JSR 310 类型之间的转换
尽管 java.util
包中的 Date
、Calendar
和 TimeZone
类型存在缺点,但它们在 Java API 中相当常见(至少在 Java 8 之前)。为了适应此类 API 的使用,Groovy 提供了在 JSR 310 类型和旧版类型之间进行转换的方法。
大多数 JSR 类型都配备了 toDate()
和 toCalendar()
方法,用于转换为相对等效的 java.util.Date
和 java.util.Calendar
值。ZoneId
和 ZoneOffset
都提供了 toTimeZone()
方法,用于转换为 java.util.TimeZone
。
// LocalDate to java.util.Date
def valentines = LocalDate.of(2018, Month.FEBRUARY, 14)
assert valentines.toDate().format('MMMM dd, yyyy') == 'February 14, 2018'
// LocalTime to java.util.Date
def noon = LocalTime.of(12, 0, 0)
assert noon.toDate().format('HH:mm:ss') == '12:00:00'
// ZoneId to java.util.TimeZone
def newYork = ZoneId.of('America/New_York')
assert newYork.toTimeZone() == TimeZone.getTimeZone('America/New_York')
// ZonedDateTime to java.util.Calendar
def valAtNoonInNY = ZonedDateTime.of(valentines, noon, newYork)
assert valAtNoonInNY.toCalendar().getTimeZone().toZoneId() == newYork
请注意,当转换为旧版类型时
-
纳秒值被截断为毫秒。例如,具有 999,999,999 纳秒的
ChronoUnit.NANOS
值的LocalTime
转换为 999 毫秒。 -
当转换“本地”类型(
LocalDate
、LocalTime
和LocalDateTime
)时,返回的Date
或Calendar
的时区将是系统默认值。 -
当转换纯时间类型(
LocalTime
或OffsetTime
)时,Date
或Calendar
的年/月/日将设置为当前日期。 -
当转换纯日期类型(
LocalDate
)时,Date
或Calendar
的时间值将被清除,即00:00:00.000
。 -
当将
OffsetDateTime
转换为Calendar
时,ZoneOffset
的小时和分钟才会转换为相应的TimeZone
。幸运的是,秒数非零的区域偏移量很少见。
Groovy 为 Date
和 Calendar
添加了许多方法,用于转换为各种 JSR 310 类型
Date legacy = Date.parse('yyyy-MM-dd HH:mm:ss.SSS', '2010-04-03 10:30:58.999')
assert legacy.toLocalDate() == LocalDate.of(2010, 4, 3)
assert legacy.toLocalTime() == LocalTime.of(10, 30, 58, 999_000_000) // 999M ns = 999ms
assert legacy.toOffsetTime().hour == 10
assert legacy.toYear() == Year.of(2010)
assert legacy.toMonth() == Month.APRIL
assert legacy.toDayOfWeek() == DayOfWeek.SATURDAY
assert legacy.toMonthDay() == MonthDay.of(Month.APRIL, 3)
assert legacy.toYearMonth() == YearMonth.of(2010, Month.APRIL)
assert legacy.toLocalDateTime().year == 2010
assert legacy.toOffsetDateTime().dayOfMonth == 3
assert legacy.toZonedDateTime().zone == ZoneId.systemDefault()
6. 实用工具
6.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
转换为 java.util.Properties
对象,该对象可以存储到 *.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'
6.2. 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'
6.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 | 从给定列表创建 ObservableList |
4 | 注册监听器 |
5 | 触发 ObservableList.ElementAddedEvent 事件 |
请注意,添加元素实际上会触发两个事件。第一个是 ObservableList.ElementAddedEvent 类型,第二个是纯 PropertyChangeEvent ,它通知侦听器 size 属性的更改。 |
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
要获取所有支持的事件类型的概述,鼓励读者查看 JavaDoc 文档或所用可观察集合的源代码。
ObservableMap
和 ObservableSet
具有与我们在本节中看到的 ObservableList
相同的概念。