###概述###
Groovy 是一门基于JVM的动态类型语言,可以与 Java 平台几乎无缝融合(与Java代码一样编译成字节码)。 使用 Groovy ,可以增强 Java 应用的灵活性和可扩展性,提升开发效率。使用 Groovy + Spock 组合也更容易编写简洁易懂的单测。熟练使用 Groovy 编程是很有益处的。
本文针对已有一定java开发经验,想要快速熟悉和使用Groovy语言的童鞋。本文会跳过变量、作用域等基础通用的编程内容,重点给出与Groovy相关的实用性的知识点。
###变量定义###
Java 代码几乎就是 Groovy 代码。Java 的变量定义基本可透明视为 Groovy 的变量定义。不加修饰符,通常默认是 public 的。一开始不熟悉 Groovy 语法的话 , 可以先用 Java 的方式来写,再逐渐替换为 Groovy 的。
Groovy 还有一种动态类型的定义符 def ,可以定义动态类型的变量。使用def关键字定义的变量类型都是Object。如下代码所示,chameleon 先后被赋值为一个整型、字符串、闭包。
def chameleon = 0
println "i am a ${chameleon.getClass().name}"
chameleon = 'haha changed'
println "now i am ${chameleon}"
chameleon = { a1, a2, op -> op(a1,a2) }
println "now i have changed to ${chameleon.getClass().name}"
println "power(2,10) = ${chameleon(2,10, {bg,p->Math.pow(bg,p)})}"
###函数调用###
####默认与可变参数#### Groovy 支持默认与可变参数,语法形式与Java相同。如下代码所示,从一个默认空列表和一个可变参数列表中获取是否有值为false的测试函数:
def static isAllTrue(List<Boolean> bList=[], boolean... bools) {
def anyContainFalse = bList?.contains(false)
if (anyContainFalse) { return false }
for (b in bools) {
if (!b) {
return false
}
}
return true
}
println "isAllTrue(true,true): ${isAllTrue (true , true)}"
println "isAllTrue(true, false, true): ${isAllTrue(true, false, true)}"
println "isAllTrue([true, false, true]): ${isAllTrue([true, false, true])}"
println "isAllTrue([true, true, true]): ${isAllTrue([true, true, true])}"
println "isAllTrue([true, true, true], true,false): ${isAllTrue([true, true, true], true,false)}"
println "isAllTrue([true, true, false], true,true): ${isAllTrue([true, true, false], true,true)}"
println "isAllTrue([true, true, true], true,true): ${isAllTrue([true, true, true], true,true)}"
####动态调用#### Groovy 支持简便的函数定义和动态函数调用,使用 "${funcName}" 来引用函数名进行调用即可。
def anoList = [3,7,2,3,1,4,6,5,9,2,8,12,6,7,1]
['unique', 'sort'].each {
println "${it} list:" + anoList."${it}"()
}
###语法精简### Groovy 做了很多针对Java语法的精简,大幅提升了开发效率。Groovy 的语法很像 Python, 而我也很喜欢 Python。
- 可以省略语句末尾的分号。 别看一小步, 积少成多,省了很多时间哦。
- 同时赋值给多个变量。很适合关联变量,比如三维坐标 def (x,y,z) = [1,2,3] ; println "($x,$y,$z)" 。
- 变量、函数声明默认为 public 的。
- 函数签名中的参数类型可省略(可能无法自动补全造成不方便)。
- 如果不存在歧义的话,函数调用可以省略括号。
- 函数的最后一个参数为闭包时,其括号可省略。
- 方法的最后一句表达式可作为返回值返回,而不需要return关键字。
- GroovyBean 默认含有 getter/setter 方法。
- with 语法创建 Context 。
- switch 可接受多种类型 。
- 所有类型都能转成布尔值,null和void相当于0或者相当于false,其他则相当于true 。
- ?. 操作符可以安全取值,有防 NPE 作用。
###字符串###
字符串是编程中最常用的对象。关于字符串,需要掌握如下三点:
创建字符串;
变量引用
正则匹配与捕获
Groovy字符串的诸多方法,可以在网上搜索,这里不再赘述。
####创建字符串####
可以使用单引号、双引号或三引号创建字符串。 单引号可以创建普通字符串,消除转义; 双引号可以创建含变量引用的字符串;三引号可以创建多行字符串。
注意: 字符串是不可变的。如果要创建可变的字符序列,使用 StringBuilder 类。
def name = 'qin'
def intro = "i am $name"
def intro2 = 'i am $name'
println "intro=$intro, intro2=$intro2"
def multilines = '''
There will be one day when
i am proud of who i am.
Because i believe my heart.
'''
println multilines
####变量引用####
变量引用是Groovy对字符串模板的重要支持,可以方便地格式化输出文本。变量引用必须围在一对双引号里(双引号可以是两个或三个')。变量引用也可以作用于方法调用。比如:
println "intro=$intro, size = ${intro.length()} intro2=$intro2 size=${intro2.length()}"
多行字符串加上变量引用特别适合做报告模板。比如下面的测试报告:
def caseInfo = """
[CaseFailed -> DataCheckFailed]
SearchTestCases: ${searchTestCases}
Expect: ${expect}
SearchParam: ${JSON.toJSONString(param)}
Data: ${JSON.toJSONString(data)}
"""
####正则表达式####
Groovy 使用 ~ 创建正则表达式,其类型是 java.util.regex.Pattern 。使用 =~ 或 ==作为匹配操作符,其中, = 是部分匹配, ==~ 是全匹配。=~ 操作符的结果是一个 Matcher 类,含有被匹配的所有字符串列表。 正则匹配示例如下:
def multilines = '''There will be one day when
i am proud of who i am.
Because i believe my heart.'''
println multilines
println multilines =~ "\\w+\\s+" ? 'matched' : 'not matched'
println multilines ==~ "\\w+\\s+" ? 'matched' : 'not matched'
def matched = multilines =~ /\w+\s+/
matched.each {
println "matched: $it"
}
def isAllMatched = multilines ==~ /(?m)^(([a-zA-z]+\s+)+([a-zA-z]+[.\n]+)\s*)+$/
println isAllMatched ? 'All matched' : 'not matched'
def matchedCaptured = multilines =~ /(?:[a-zA-z]+\s+)+(?:[a-zA-z]+[.\n]+)/
matchedCaptured.each {
println "matchedCaptured: $it"
}
有几点说明下:
正则表达式可以使用单引号、双引号或双斜线//围起来。使用双斜线时,可以对反斜杠转义,双引号下 "\w+" 可以写成 /\w+/ ;
多行匹配使用 ?m ,忽略捕获分组使用 ?:
编写正确的正则表达式,我没有固定经验可循,只有不断尝试,从最简单的字符串匹配,依次编写子表达式匹配字符串里每个子部分,然后将子表达式组合起来即可。组合的方式有顺序、+,*,?, [] 等。
匹配正确后,可以使用 each 遍历匹配的字符串。
关于正则表达式的基础知识,可参阅:“正则表达式基础知识”
###闭包###
闭包和元编程是Groovy语言的两大精髓。Groovy的闭包大大简化了容器的遍历,提升了代码的可扩展性,使代码更加简洁优雅。闭包在Groovy编程中几乎无处不在。
可参阅: “谈谈Groovy闭包”
###容器###
任何一门编程语言中,容器的使用是重中之重。灵活使用Map,List 可以让代码实现更简洁。
####Map####
Map 的创建非常简单,中括号 [] 里面包含一系列的 key:value 即可。跟Python的语法一样简洁。通过 def map = [key:value] 创建的Map类型是 java.util.LinkedHashMap 。 访问Map中的元素,可以用 map[key] , map.key (key含特殊字符不可以), map.'key' , 或 map.get(key), map.getOrDefault(key) 。
Map 的基本方法有 each, collect, find, findAll ,分别用来做最通用的遍历、收集器、查询过滤操作。
println map['me']
println map.lover
println map.getOrDefault("nonexist", [:])
println map.collect { it.value['name'] }
println map.findAll { it.value.age <=25 }
####List####
List 的创建非常简单,中括号里包含逗号分隔的一系列元素。跟大多数语言里的数组创建一样。通过def list = [1,2,3] 创建的List 类型是 java.util.ArrayList。
下标访问
使用下标访问List中的元素很灵活。可以使用 list[i], list.get(i), 可以使用区间下标,负数下标。如下所示:
println "first: " + alist.get(0)
println "second: " + alist[1]
println alist[2..4]
println alist[-1]
常用方法
list 也有非常丰富的方法来操作其中的元素。each, eachWithIndex, find, findAll, collect, groupBy, join, flatten, inject 等,这些参数都是一个闭包。
eachWithIndex可以按照下标遍历列表元素; find, findAll 在列表中查询满足条件的第一个元素或所有元素列表;collect 可以将列表元素转换成另一个列表;groupBy可以根据某种条件对列表元素分组;join可以连接列表元素成字符串;flatten 可以将嵌套列表打平成一维列表;inject 类似 reduce 方法。
alist.eachWithIndex {
int entry, int i ->
println "index=$i, item=$entry"
}
println alist.findAll {
it % 3 == 0
}
println alist.collect { it * it }
println '[' + alist.join(",") + ']'
println alist.inject(1) { n1, n2 -> n1 * n2 }
println ([[1,2,4], [1,3,9], [1,4,16]].flatten())
println ([[[1,5,10], [1,6,12]], []].flatten())
###对象###
定义一个Groovy类很简单,几乎只要指定属性名即可。def 默认为属性添加 getter/setter 方法。
class Person {
def name
def age
def address
}
class Address {
def detail
}
然后可以基于Map来创建对象。一般使用 List 来存储多个对象,然后通过 List 方法来操作对象列表。访问对象的属性用 object.attrName 即可。如下代码所示:
def persons = [new Person(["name": 'qin', "age": 28, "address": new Address(["detail": "Hangzhou"])]), new Person(["name": 'ni', "age": 25])]
println persons.collect { "${it.name} live in ${it.address?.getDetail()}" }
println persons.find { it.age >=28 }.name
这里使用了 ?. 操作符对对象做非空判断。一般用于访问嵌套对象中某个子对象的属性而该子对象可能不存在的情况。 ?. 必须放置在可能不存在的子对象的后面,表示对这个对象的存在性“表示怀疑”。这样就免除了写 if-else 判断的繁琐,尤其在多层嵌套对象的时候更为方便。另外也可以看到,变量引用也可以作用于对象的属性和方法。
Groovy对象也支持动态方法调用。只要使用 objectRef."${methodName}"(args) 即可。
persons[0].class.declaredMethods.findAll {
it.parameterCount == 0
}.each {
println "method = ${it.name}, callValue = ${persons[0]."${it.name}"()}"
}
###文件读写###
Groovy对文件读写提供了非常方便的支持,不需要像Java那样new一堆的实现细节类(应该有一个Facade类),还需要谨慎地打开和关闭文件。
读文件的代码如下:分别是读取整个文本、读取每一行并进行指定处理、将读取的每行存入列表。
File file = new File("README.md")
println file.text
file.eachLine("UTF-8") {
println it
}
println file.readLines()
写文件的代码如下(使用了with语法):
new File("/tmp/result.txt").withPrintWriter { printWriter ->
printWriter.println('The first content of file')
}
是如下写法的简化版:
def printWriter = new File("/tmp/result2.txt").newPrintWriter()
printWriter.write('The first content of file')
printWriter.flush()
printWriter.close()
遍历目录主要是使用过滤器和闭包来遍历匹配的所有文件或目录并进行处理。注意,eachFileRecurse 会递归遍历该目录下所有子目录的所有文件,而 eachFileMatch 则只遍历该目录下的文件而不包含递归子目录的文件。
path.eachFileRecurse(FileType.FILES) {
def filename = it.name
if (filename =~ ~/.*\.groovy$/) {
println filename
}
}
path.eachFileMatch(~/.*\..*/) {
println it.name
}
path.eachDirRecurse {
def filename = it.canonicalPath
if (filename =~ ~/.*groovy$/) {
println filename
}
}
###元编程与反射### 闭包和元编程是Groovy语言的两大精髓。元编程的内容广泛,会专门用一篇文章来讲解。目前读者可以先看简书上的一篇文章:“Groovy学习之-运行时元编程”。
Groovy元编程的最简单用法,就是便捷地实现 Java 的反射机制,关键类是:metaClass。如下代码所示:
def propertiesInPerson = Person.metaClass.properties.collect { it.name }
println "propertiesInPerson=${propertiesInPerson}"
persons.each {
person ->
def personName = person.invokeMethod("getName", null)
def personAge = person.metaClass.invokeMethod(person, "getAge", null)
println "${personName} ages ${personAge} "
}
熟练使用 metaClass 和 invokeMethod 是Groovy元编程的入门。
###参考文献###