Groovy 开发工具包

1. 使用 IO

Groovy 提供了许多用于处理 I/O 的 辅助方法。虽然你可以在 Groovy 中使用标准的 Java 代码来处理这些,但 Groovy 提供了更方便的方法来处理文件、流、阅读器等等。

特别是,你应该看一下添加到

以下部分重点介绍使用上述辅助方法的示例惯用结构,但并非旨在完全描述所有可用方法。有关这些内容,请阅读 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.DataOutputStreamjava.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 实例,该实例随后将允许处理 in/out/err 流,并检查进程的退出值等等。

例如,以下是与上面相同的命令,但现在我们将一次处理结果流的一行

def process = "ls -l".execute()             (1)
process.in.eachLine { line ->               (2)
    println line                            (3)
}
1 在外部进程中执行 ls 命令
2 对于进程的输入流中的每一行
3 打印该行

值得注意的是,in 对应于命令的标准输出的输入流。out 将引用一个流,你可以通过它向进程发送数据(它的标准输入)。

请记住,许多命令是 shell 内置命令,需要特殊处理。因此,如果你想在 Windows 机器上列出目录中的文件并写入

def process = "dir".execute()
println "${process.text}"

你将收到一个 IOException,提示 无法运行程序“dir”:CreateProcess 错误=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 也有使用 StringBufferInputStreamOutputStream 等等的变体。有关完整列表,请阅读 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. 遍历列表

遍历列表中的元素通常通过调用 eacheachWithIndex 方法来完成,这些方法会对列表中的每个项目执行代码

[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. 操作列表

过滤和搜索

the 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']

然而,重要的是,列表上的 + 运算符是 **非变异的**。与 << 相比,它会创建一个新列表,这通常不是您想要的,并且会导致性能问题。

the 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]

当然,removeAtremoveElement 将适用于任何类型的列表。

此外,可以通过调用 clear 方法来删除列表中的所有元素

def list= ['a',2,'c',4]
list.clear()
assert list == []
集合运算

the 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]
复制元素

the 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。这意味着 foo.class 将在不包含 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 开发工具包 中,对映射的习惯性迭代利用了 eacheachWithIndex 方法。值得注意的是,使用映射字面量表示法创建的映射是 **有序的**,也就是说,如果您迭代映射条目,则保证条目将按照它们添加到映射中的顺序返回。

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 == [:]

使用映射字面量语法生成的映射使用对象 equalshashcode 方法。这意味着您 **决不** 应该使用哈希码会随时间变化的对象,否则您将无法获取相关的值。

同样值得注意的是,您 **决不** 应该使用 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 中的集合,这些集合通常不能保证可以通过 keySetentrySetvalues 安全地操作集合。

过滤和搜索

the 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

使用 .. 表示法定义的范围是包含式的(即列表包含 from 和 to 值)。

使用 ..< 表示法定义的范围是半开式的,它们包含第一个值但不包含最后一个值。

使用 <.. 表示法定义的范围也是半开式的,它们包含最后一个值但不包含第一个值。

使用 <..< 表示法定义的范围是全开式的,它们既不包含第一个值,也不包含最后一个值。

// 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

请注意,int 范围是高效实现的,创建了一个轻量级的 Java 对象,其中包含 from 和 to 值。

范围可以用于实现 java.lang.Comparable 的任何 Java 对象,以便进行比较,并且还具有 next()previous() 方法来返回范围内下一个/上一个项目。例如,您可以创建 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}"
}

但您可以通过使用 each 方法遍历范围来以更具 Groovy 特色的风格实现相同的效果

(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 == "!"

您可以使用负索引从 List、数组、字符串等的末尾开始计数。

def name = text[-7..-2]
assert name == "gromit"

最后,如果您使用的是反向范围(起始索引大于结束索引),那么答案将被反转。

text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"

2.5. 增强集合方法

除了列表映射范围,Groovy 还提供许多用于过滤、收集、分组、计数等的操作方法,这些方法可以直接在集合或更易迭代的对象上使用。

特别是,我们建议您阅读Groovy 开发工具包 的 API 文档,特别是:

  • 添加到 Iterable 的方法可以在这里找到

  • 添加到 Iterator 的方法可以在这里找到

  • 添加到 Collection 的方法可以在这里找到

  • 添加到 List 的方法可以在这里找到

  • 添加到 Map 的方法可以在这里找到

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. 在列表上迭代

遍历列表中的元素通常通过调用 eacheachWithIndex 方法来完成,这些方法会对列表中的每个项目执行代码

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 的传统 DateCalendar 类进行操作的许多扩展功能。

您可以使用普通的数组索引表示法访问 DateCalendar 的属性,并使用 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 支持对 DateCalendar 实例进行算术运算和迭代,如以下示例所示

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

5. 使用 Date/Time 类型

groovy-datetime 模块支持使用 Java 8 中引入的Date/Time API 进行操作的许多扩展功能。本文档将此 API 定义的数据类型称为“JSR 310 类型”。

5.1. 格式化和解析

使用日期/时间类型时,常见的用例是将它们转换为字符串(格式化)和从字符串转换(解析)。Groovy 提供了以下额外的格式化方法:

方法 描述 示例

getDateString()

对于 LocalDateLocalDateTime,使用 DateTimeFormatter.ISO_LOCAL_DATE 格式化

2018-03-10

对于 OffsetDateTime,使用 DateTimeFormatter.ISO_OFFSET_DATE 格式化

2018-03-10+04:00

对于 ZonedDateTime,使用 DateTimeFormatter.ISO_LOCAL_DATE 格式化,并附加 ZoneId 的简短名称

2018-03-10EST

getDateTimeString()

对于 LocalDateTime,使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME 格式化

2018-03-10T20:30:45

对于 OffsetDateTime,使用 DateTimeFormatter.ISO_OFFSET_DATE_TIME 格式化

2018-03-10T20:30:45+04:00

对于 ZonedDateTime,使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME 格式化,并附加 ZoneId 的简短名称

2018-03-10T20:30:45EST

getTimeString()

对于 LocalTimeLocalDateTime,使用 DateTimeFormatter.ISO_LOCAL_TIME 格式化

20:30:45

对于 OffsetTimeOffsetDateTime,使用 DateTimeFormatter.ISO_OFFSET_TIME 格式化器

20:30:45+04:00

对于 ZonedDateTime,使用 DateTimeFormatter.ISO_LOCAL_TIME 格式化,并附加 ZoneId 的简短名称

20:30:45EST

format(FormatStyle style)

对于 LocalTimeOffsetTime,使用 DateTimeFormatter.ofLocalizedTime(style) 格式化

4:30 AM(例如,使用样式 FormatStyle.SHORT

对于 LocalDate,使用 DateTimeFormatter.ofLocalizedDate(style) 格式化

Saturday, March 10, 2018(例如,使用样式 FormatStyle.FULL

对于 LocalDateTimeOffsetDateTimeZonedDateTime,使用 DateTimeFormatter.ofLocalizedDateTime(style) 格式化

Mar 10, 2019 4:30:45 AM(例如,使用样式 FormatStyle.MEDIUM

format(String pattern)

使用 DateTimeFormatter.ofPattern(pattern) 格式化

03/10/2018(例如,使用模式 ’MM/dd/yyyy’)

对于解析,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 方法保持一致。

5.2. 操作日期/时间

5.2.1. 加法和减法

Temporal 类型具有 plusminus 方法,用于添加或减去提供的 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 提供了额外的 plusminus 方法,这些方法接受一个整数参数,使上述代码可以更简洁地重写

def nextAprilFools = aprilFools + 365 // add 365 days
def idesOfMarch = aprilFools - 17 // subtract 17 days

这些整数的单位取决于 JSR 310 类型操作数。如上所示,用于 ChronoLocalDate 类型(如 LocalDate)的整数单位为 。用于 YearYearMonth 的整数单位分别为 。所有其他类型的单位为 ,例如 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. 乘法和除法

* 运算符可用于将 PeriodDuration 实例乘以整数值;/ 运算符可用于将 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. 取反

DurationPeriod 类型表示负或正的时间长度。可以使用一元 - 运算符对它们进行取反。

def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15

5.3. 与日期/时间值交互

5.3.1. 属性表示法

TemporalAccessor 类型(例如 LocalDateLocalTimeZonedDateTime 等)的 getLong(TemporalField) 方法和 TemporalAmount 类型(即 PeriodDuration)的 get(TemporalUnit) 方法可以使用 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. 范围、uptodownto

JSR 310 类型可以与 范围运算符 一起使用。以下示例在今天和 6 天后的 LocalDate 之间迭代,并为每次迭代打印出星期几。由于两个范围边界都包含在内,因此它会打印出所有 7 天。

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 值。

uptodownto 和范围的迭代单位与加法和减法的单位相同: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 日期时,迭代将立即停止。

简而言之,当使用自定义迭代单位使用 uptodownto 方法进行迭代时,当前迭代值永远不会超过结束值。

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. 创建周期和持续时间

右移运算符 (>>) 用于生成一个表示操作数之间时间段或持续时间的数值。对于 ChronoLocalDateYearMonthYear,该运算符将返回一个 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 包中的 DateCalendarTimeZone 类型存在缺点,但它们在 Java API 中(至少在 Java 8 之前的 API 中)非常常见。为了适应此类 API 的使用,Groovy 提供了在 JSR 310 类型和传统类型之间进行转换的方法。

大多数 JSR 类型都配备了 toDate()toCalendar() 方法,用于转换为等效的 java.util.Datejava.util.Calendar 值。ZoneIdZoneOffset 都提供了 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

请注意,在转换为传统类型时:

  • 纳秒值将被截断为毫秒。例如,LocalTimeChronoUnit.NANOS 值为 999,999,999 纳秒,将转换为 999 毫秒。

  • 在转换“本地”类型 (LocalDateLocalTimeLocalDateTime) 时,返回的 DateCalendar 的时区将为系统默认时区。

  • 在转换仅时间类型 (LocalTimeOffsetTime) 时,DateCalendar 的年/月/日将设置为当前日期。

  • 在转换仅日期类型 (LocalDate) 时,DateCalendar 的时间值将被清除,即 00:00:00.000

  • 在将 OffsetDateTime 转换为 Calendar 时,只有 ZoneOffset 的小时和分钟会转换为相应的 TimeZone。幸运的是,具有非零秒的时区偏移量很少见。

Groovy 在 DateCalendar 中添加了一些方法,用于转换为各种 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 还支持 environmentsenvironments 方法可用于传递一个闭包实例,该实例本身可能包含多个部分。假设我们想为开发环境创建一个特定的配置值。在创建 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 注册侦听器
4 从给定的列表创建 ObservableList
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 文档或正在使用的可观察集合的源代码。

ObservableMapObservableSet 的概念与我们在本节中介绍的 ObservableList 相同。