Groovy学习笔记
Groovy诞生于2004年, 它是可选类型的(optionally typed)动态语言,是最流行的JVM语言之一,比起其它语言,Groovy能够与Java无缝集成、相互调用,这是它的最大优势。
Groovy可以作为脚本语言使用(但是执行时,即便是脚本也会编译为Java类),也可以编译为Java类。在运行时,你只需要把 groovy-all-*.jar 放在Classpath中,无需其它任何依赖或者执行引擎。
使用Groovy,你可以:
- 让Java代码更加简洁
- 自动化重复的任务
- 使用DSLs(domain-specific languages)进行业务建模
- 执行即席的脚本任务
Groovy有以下高级特性:
- 支持闭包(Closures)
- 支持动态方法(dynamic methods)
- 为Java平台引入元对象协议(MOP,Meta Object Protocol)
可以到官网下载Groovy运行时,并将安装目录设置为环境变量 GROOVY_HOME ,同时把 $GROOVY_HOME/bin 添加到环境变量PATH中。
工具 | 说明 |
groovysh | 启动交互式Shell |
groovyConsole | 运行一个GUI来交互执行Groovy代码 |
groovy | 开始解释Groovy脚本。作为参数的脚本只需要在Classpath中存在即可 |
- 预编译模式(Precompiled mode):使用Groovy编译器——groovyc,把*.groovy编译为*.class文件。并把*.class放置到Classpath下,通过Java的类加载器加载这些类并运行
- 直接模式(Direct mode):通过groovy的类加载器,在运行时直接加载*.groovy文件,并动态的生成JVM对象,这种情况下,没有生成.class文件,但是在JVM中是有对应的java.lang.Class实例的
Groovy在源代码级别增强了Java,但是两者的字节码格式是完全相同的。
GDK即Groovy Development Kit,是Groovy对JDK的补充,引入了一些新的类,并为已经现有的一些Java类增强了功能。下面列出GDK的一些特性:
- 为所有类型引入 size() 方法
- 简化的数据库访问
- 简化的XML处理
动态性是指在运行时修改程序结构的能力。
Groovy生成的字节码,会在运行时转换为普通的Java类。除非使用特殊的机制影响JVM,那么这些已加载的Java类的结构是无法动态改变的。
Groovy为什么能具有动态语言的特征,是因为其生成的字节码,不是直接调用方法,而是像这样:
1 |
getMetaClass().invokeMethod(this, "method", EMPTY_PARAMS_ARRAY) |
也就是说,方法调用以反射的方式转给对象的MetaClass进行处理, 这就类似于Cglib等字节码增强工具一样,可以在运行时拦截、重定向、增加/删除方法。
Groovy和Java在语法上的共同点包括:
- 相同的包处理机制(包括包的声明和import语句)
- 类和方法的定义(嵌套类除外)
- 控制结构语句(经典的C风格for循环除外)
- 操作符、表达式和赋值
- 异常处理
- 变量声明
- 对象实例化,引用和取消引用对象
- 方法调用
Groovy和Java在语法上的差异点包括:
- 非静态内部类的实例化语法:
1new OuterClass.NonStaticInnerClass( outClassInstance,args )
Groovy还引入了新的语法特性:
- 通过新的表达式和操作符访问Java对象
- 更多的对象声明方式
- 引入新的控制结构
- 引入新的数据类型、操作符和表达式
- 顶级表达式中的方法调用的括号可以省略,前提是被调用方法有至少一个参数(这是为了防止与属性访问歧义,该限制可以通过元编程突破)。链式方法调用与该规则配合使用,可以形成自然语言风格,在DSL中常用
- 作为最后一个入参的闭包,可以放在方法调用括号的外面,这一规则可以和上一条联用
- 方法重载发生在运行期而非编译期
1 2 3 4 5 6 7 8 |
groovy.lang.* groovy.util.* java.lang.* java.util.* java.net.* java.io.* java.math.BigInteger java.math.BigDecimal |
上述包和类自动导入,而Java仅仅导入java.lang包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
package cc.gmem.study.groovy //声明类 class Person { //声明成员变量 private int id //声明一个属性 String name /** * GroovyDoc注释 * @param name 名称 */ Person(String name) { this.name = name } /* 程序入口点:main方法 */ static main(args) { //方法调用的括号,在不引起歧义的前提下可以省略 System.out.println 'Groovy' //字符串可以用单引号界定 // 多个参数 httpRequest "https://blog.gmem.cc", "application/json" // 命令参数 httpRequest url: "https://blog.gmem.cc", acceptType:"application/json" 1.equals( 1 ) //万物皆对象 //断言 assert 1 == 1 // ==调用equals()方法进行判断,而不是比较对象地址 xx = 1 // 如果局部变量xx不存在,等价于this.xx = 1 //def用于声明“动态类型” def x = 1 //打印变量的值 println x def person = new Person('Alex') person.setName( 'Alex Wong' ) //getter/setter直接可以用,不需要编写方法 person.name = 'Alex Wong' //与上面效果一样,会访问setter而不是直接访问属性 def sls = '单引号字符串等价于java.lang.String' char a = 'x' //字符类型必须静态声明,或者 'x' as char //groovy.lang.GString:双引号字符串中的占位符可以动态解释,在必要时,Groovy可以在GString和String之间进行转换 println "Hello $person.name" //占位符以$开始,支持属性导航,可以使用${}形式,{}中可以是任何表达式 def mls = '''多行 字符串''' //语言级别的正则式支持 // =~表示查找操作符 assert person.name =~ /\sWong/ //斜线界定的字符串,表示不转义反斜杠(\),在正则式中特别有用 assert 'Alex Wang' == person.name.replaceAll( /Wong/, 'Wang' ) //替换 //java.math.BigInteger、java.math.BigDecimal直接量 def bigInt = 1000g def bigDec = 1000.01g //列表直接量 def roman = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] assert roman[4] == 'IV' //数字索引语法访问元素 roman[8] = 'VIII' //自动扩展列表 //映射(字典直接量) def http = [ 100 : 'CONTINUE', 200 : 'OK', ] assert http[200] == 'OK' http[400] = 'BAD REQUEST' //动态插入键值对 //语言级别的范围(range)支持 def range = 1..10 assert range.contains(5) assert range.contains(15) == false assert range.size() == 10 assert range.from == 1 assert range.to == 10 assert range.reverse() == 10..1 //闭包支持,闭包是花括号限定的代码块,其入参列表以 ->结束 def c = {el -> println el } [1, 2, 3].each (c) } } |
操作符 | 对应方法 | 适用于 | ||
a + b | a.plus(b) | Number, String,StringBuffer, Collection,Map, Date, Duration | ||
a – b | a.minus(b) |
Number, String, List, Set, Date, Duration
|
||
a * b | a.multiply(b) | Number, String, Collection | ||
a / b | a.div(b) | Number | ||
a % b | a.mod(b) | Integral number | ||
a++ (++a) | a.next() | Iterator, Number, String, Date, Range | ||
a-- (--a) | a.previous() | Iterator, Number, String, Date, Range | ||
-a | a.unaryMinus() | Number, ArrayList | ||
+a | a.unaryPlus() | Number, ArrayList | ||
a ** b | a.power(b) | Number | ||
a | b | a.or(b) | Number, Boolean, BitSet, Process | ||
a & b | a.and(b) | Number, Boolean, BitSet | ||
a ^ b | a.xor(b) | Number, Boolean, BitSet | ||
~a | a.bitwiseNegate() | Number, String(返回正则式) | ||
a[b] | a.getAt(b) | Object, List, Map, CharSequence, Matcher | ||
a[b] = c | a.putAt(b, c) | Object, List, Map, StringBuffer | ||
a << b | a.leftShift(b) | integral number, StringBuffer, Writer, File, Socket, List | ||
a >> b | a.rightShift(b) | Number | ||
a >>> b | a.rightShiftUnsigned(b) | Number | ||
a == b | a.equals(b) | Object | ||
a != b | !(a == b) | Object | ||
a <=> b | a.compareTo(b) | java.lang.Comparable | ||
a > b | a.compareTo(b) > 0 | |||
a >= b | a.compareTo(b) >= 0 | |||
a < b | a.compareTo(b) < 0 | |||
a <= b | a.compareTo(b) <= 0 | |||
a as type | a.asType (typeClass) | Any type |
自己实现操作符重载时,可以重载上述方法,以便针对具体类型实现二元操作符,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Currency { int rate int amount Currency(int rate,int amount) { this.rate = rate this.amount = amount } Currency plus(Currency c) { //注意这里体现了Groovy的类型转换原则:返回一个更一般的类型 def add = (amount + c.amount*c.rate / rate) as int return new Currency(rate, add) } Currency plus(Integer c) { return new Currency(rate, amount + c) } @Override public String toString() { return amount } } def usd = new Currency(6, 60 ) def cny = new Currency(1, 60 ) println usd + cny println usd + 60 |
Groovy在语言层面支持一套数据类型,包括字符串、正则式、数字、范围、列表、映射等。此外,不像Java,Groovy不存在所谓基本类型。
在groovy中,万事万物皆对象,这与Java不同,后者包含若干基本类型(int/double/char/byte),基本类型是专有类型,其它类型是引用类型。 这些专有类型:
- 其变量存放了数据本身,而不是数据的地址
- 不能应用面向对象的编程风格——方法调用、属性导航。不能从java.lang.Object继承方法
- 能应用多种运算符,而对应的引用类型则基本不能应用这些运算符
Groovy扩展了Java数字类型的直接量表示:
Java类型 | 直接量语法 |
java.math.BigInteger | 612g,1106G |
java.math.BigDecimal | 3.14, 6.02E23, 94605.56G |
Groovy调用Java方法时,数字类型的拆装箱完全自动进行。
Groovy的变量声明可以没有类型信息,以 def 开头声明的对象,称为动态类型,动态类型指向的对象的类型可以动态变化:
1 2 3 4 5 6 7 |
def dynaVar = 10G //改变对象类型 dynaVar = "$dynaVar" //如果声明为静态类型,则不能改变 BigDecimal bd = 10G bd = "" // 报错:Cannot cast object '' with class 'java.lang.String' to class 'java.math.BigDecimal' |
Groovy引入了 groovy.lang.GString 类型,用来支持占位符替换。Groovy支持多种字符串表示形式:
- 单引号包围的字符串:不支持占位符替换,相当于Java的字符串。如果要表示 char 类型,则必须静态声明或者使用 as 操作符Cast
- 双引号包围的字符缓存:支持占位符变量替换,占位符形式为: $varName 或 ${obj.propName}
- 三引号包围的字符串:支持多行。如果是三个双引号,则支持占位符替换
- 斜杠包围的字符串:不需要对反斜杠进行转义,主要用于正则式
Groovy为字符串引入了一些新方法,包括: toInteger, toLong, toFloat, toDouble 等。
可以对字符串进行追加、修改等操作:
1 2 3 4 5 6 7 8 |
greeting = 'Hello' greeting <<= ' Groovy' //可以对字符串进行内容追加 //追加后自动转换为StringBuffer assert greeting instanceof java.lang.StringBuffer greeting << '!' assert greeting.toString() == 'Hello Groovy!' //不会自动转回String,因此需要调用toString() greeting[1..4] = 'i' //可以用这种语法替换范围指定的子串 assert greeting.toString() == 'Hi Groovy!' |
Groovy引入了三个操作符,专用于正则式处理
操作符 | 说明 |
=~ | 正则式匹配操作符 |
==~ | 正则式匹配整串操作符 |
~String | 正则式模式(Pattern)操作符,把字符串转换为java.util.regex.Pattern类型 |
下面是一些示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import java.util.regex.Matcher; def pattern = /W.ng/ def name = 'Alex Wong Wang' def lname = 'Wang' //完整匹配,返回值是一个Boolean对象 matcher = lname ==~ pattern assert matcher instanceof Boolean //子串匹配,返回值是一个matcher对象 matcher = ( name =~ pattern ) assert matcher instanceof Matcher //支持使用索引语法访问匹配的子串 assert matcher[0] == 'Wong' assert matcher.count == 2 //预编译正则式 pattern = ~/W.ng/ pattern.matcher( 'Alex Wong' )[0] |
Groovy中的数字处理更加直观和符合预期,例如 1/2 的结果会是0.5而非0。
在Groovy中, + - * 运算满足下列规则:
- 如果有一个数为Float或者Double,那么结果是Double
- 否则,如果一个操作数为BigDecimal,结果为BigDecimal
- 否则,如果一个操作数为BigInteger,结果为BigInteger
- 否则,如果一个操作数为Long,那么结果为Long
- 否则,结果为一个Integer
对于 / 运算:
- 如果任何一个数是Float(或者Double)类型,那么运算结果为Double类型
- 否则,结果为BigDecimal类型,精度以两个数的精度值较大的为准,采用四舍五入的方式,结尾没有无效的0
- 整数除法可以使用 as 转换,或者使用 intdiv() 方法
当运算结果超过类型表示范围后,不会自动提升类型,幂运算除外。幂运算根据需要依 Integer,Long,Double 的次序提升。
Groovy引入一个groovy.lang.Range类,表示一个连续的对象范围,并在语法级别上支持范围的直接量:
1 2 3 4 5 6 7 8 |
left = 0 right = 10 Range r = left..right r = (left..right) //小于号表示右排除范围,即范围不包含right这个值 r = (left..<right) //起始值大于结束值的范围称为“反向范围” r = 10..0 |
范围的元素不一定是数字。 日期、字符也可以:
1 2 3 4 5 |
assert ('a'..'d').size() == 4 def today = new Date() def tomorrow = today + 1 assert (today..tomorrow).size() == 2 |
Range的 toList() 方法用于生成对应的列表。
可以使用下面的方式遍历Range:
1 2 3 4 |
//遍历Range for ( day in today..tomorrow) println day //使用闭包遍历 (today..tomorrow).each { day -> println day } |
Groovy把Java数字和List的优点融合到了一起:基于索引的操作、动态增减元素。Groovy在语法上修改了List直接量的声明方式:
1 |
list = [1, 2, 3] |
Groovy缺省使用的List实现是 java.util.ArrayList ,你可以显示声明为其它类型。 Groovy为List重载了 [] 操作符,支持多种下标访问风格:
1 2 3 4 5 6 7 8 9 10 11 |
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] //基于Range的范围访问 assert list[2..4] == [2, 3, 4] //getAt //基于索引集合的访问 assert list[2, 3, 4] == [2, 3, 4] list[2..4] = [2, 3, 4] //setAt 设置元素 list[5..9] = [] //移除元素 println list // [0, 1, 2, 3, 4] list[1..<1] = [9, 9, 9] //在索引1上插入多个元素 println list // [0, 9, 9, 9, 2, 3, 4] |
List支持多种操作符:
1 2 3 4 5 6 |
list = [] list += 1 // [1] list += [2, 3] // [1, 2, 3] list << 4 << 5 // [1, 2, 3, 4, 5] list -= [1, 2, 3, 4] // [5] list * 3 // [5, 5, 5] |
与List类似,Groovy也从语法上支持Map。和其它语言使用的花括号不同,Groovy用方括号界定Map:
1 2 3 4 |
map = [ 'Alex' : 'Wong', 'Meng' : 'Lee' ] |
如果要声明一个空的Map,使用 [:] 。Groovy默认使用java.util.HashMap。由于Map的Key往往是字符串,因此,Key的字符串标记可以省略: assert ['a':1] == [a:1] ,但是Key不能是Groovy关键字或者包含特殊符号。
除了数组语法外,还可以使用点号导航来访问Map元素:
1 2 |
myMap = ['a.b':1] assert myMap.'a.b' == 1 |
所谓闭包(Closure),就是绑定了上下文中变量的匿名函数。闭包在很多语言中都可以见到,但是在Java领域,直到Java 8才通过Lambda语法提供闭包特性。Groovy引入 groovy.lang.Closure 表示闭包,并在语法层面上支持闭包的声明。闭包是普通对象,可以被传递。声明一个闭包很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
list = [1, 2, 3] counter = 0 // 闭包语法: {入参列表 -> 语句列表} Closure c = {val -> counter++; ++val} //闭包的最后一个表达式,作为默认的返回值,任何时候可以显式的return list.each c // 匿名的闭包 list.each {val -> ++val} // 小箭头也可以省略 def c = { println it } // 作为参数的闭包,可以使用注解说明其入参类型,这些注解主要供IDE的智能提示 static int shell(@ClosureParams( value = SimpleType.class, options = "java.io.Reader" ) Closure stdoutReader){} // 多参数闭包入参类型提示 class Lists { static <T,U> List<T> randomInstances(List<U> listToIterate, final Builder<T> builder, @ClosureParams(value = FromString, options = ["T,U", "T"]) final Closure<T> postProcessor = null) { } } } // 闭包的返回值,可以在其泛型参数中声明 public abstract class Closure<V> extends GroovyObjectSupport{} |
注意,闭包具有访问上下文中变量的能力(上面第4行),这意味着上下文变量以引用的形式(而不是值)传递给闭包。特别是,即使上下文变量的Scope已经终结,被闭包引用的那些变量依然存在:
1 2 3 4 5 6 7 |
def genCloseure(){ def i = 10 return {println i++} } def c = genCloseure() //i作为局部变量,正常情况下应该在调用完毕即销毁 c() // 10,i还存活着 c() // 11 |
在闭包中,关键字:
- this 表示声明闭包的对象或者类
- owner 表示直接包含闭包的对象或类
- delegate 默认值为owner,该变量用于重新定位闭包体中引用的上下文变量,将它们解析为自己的属性/方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package cc.gmem.study.groovy def cc = { // 外层闭包 return { // 内层闭包 if (printhis){ println this //当前对象cc.gmem.study.groovy.GroovyStudy@4485af5c println owner //外层闭包cc.gmem.study.groovy.GroovyStudy$_run_closure1@15b734b println delegate //默认就是owner,可以运行时替换 }else{ println name //自动解析的上下文变量 } } } def c = cc() printhis = true c() printhis = false // 修改delegate对象 c.delegate = [name : 'DelegateObject'] c() // 打印DelegateObject c.name = 'InnerClosure' //设置内层闭包的属性 /** * 下面两行打印: * name of inner closure is InnerClosure * name of enclosing closure is InnerClosure * name of current object is InnerClosure * 可以看到,设置闭包的xxx属性,那么owner.xxx也被设置,且这一过程会递归进行 * 在闭包里,this.xxx和owner.xxx是同义词,但是this和owner则可能不是一个对象 */ println "name of inner closure is $c.name" println "name of enclosing closure is $cc.name" println "name of current object is $name" c() // 打印InnerClosure |
设置闭包的 resolveStrategy 属性可以改变默认的上下文变量搜索规则:
- OWNER_FIRST :默认,优先搜索owner
- OWNER_ONLY :仅搜索owner
- DELEGATE_FIRST :优先搜索delegate
- DELEGATE_ONLY :仅搜索delegate
- TO_SELF :owner、delegate都不搜索
当然,你随时可以使用 this 、 owner 、 delegate 显式的访问特定上下文对象的属性,不受上述搜索规则限制。此外,局部变量的优先级比上面的上下文对象高:
- 当前闭包自己定义的局部变量优先级最高
- 定义了当前闭包的函数/闭包——即owner——其局部变量的优先级次之。这是闭包的重要特点:在声明时绑定(back reference)owner定义的局部变量
- 多层函数/闭包嵌套时,越外层的局部变量,优先级越低
- 找不到局部变量,则在this、owner、delegate等上下文对象中寻找属性
如果闭包只有一个参数,那么不需要显式声明,Groovy允许使用 it 来访问唯一入参。
可以把对象方法(不支持静态方法)作为闭包引用: def closure = reference.&methodName 。Groovy支持在运行时进行重载闭包引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//方法闭包限制在实例方法上 class Lad{ def sayHello(){ sayHello('Hello') } def sayHello(def hello){ println hello } } def c = new Lad().&sayHello //c引用哪个方法在此并不确定 // 运行时重载示意: // 闭包支持直接“调用”,这又是操作符重载的威力。相比之下Java 8 Lambda则不能直接“调用” c() // 零参版本 c('Hi') // 一参版本 |
Groovy为闭包提供了以下重要方法:
方法 | 说明 | ||
call() | 调用闭包,与()操作符一样 | ||
curry() |
返回当前闭包绑定了若干参数(从左向右)的副本闭包:
|
||
isCase() |
闭包实现了该方法,可以作为switch语句的分类器使用:
|
在Groovy中可以和Java一样,声明和使用注解。下表列出重要的内置注解:
注解 | 说明 | ||
@Immutable |
标记一个类型为不变类型:为class和所有字段添加final标记,对于那些本身可变的对象,Groovy会强制defensive copying。例如:
|
||
@Grab |
在脚本中显式的声明依赖,使其自包含。
IDE或者Maven、Ivy等工具负责识别和定位依赖 |
||
@Category |
用于声明一个Category,注解参数说明this指向的类型:
|
||
@Mixin |
轻松的把其他类定义的方法添加到某个类中:
该特性自2.3开始已经废弃,由 trait 代替 |
||
@Delegate |
自动添加被代理对象的全部行为到当前类:
在实现装饰、代理等模式时,可以避免写大量重复性代码 |
运行时数据类型 | 何时作为true处理 |
Boolean | 如果对应的布尔值为true |
Matcher | 如果存在至少一个匹配 |
Collection | 如果集合非空 |
Map | 如果映射非空 |
String, GString | 如果字符串非空 |
Number, Character | 如果值为非0 |
其它所有类型 | 如果为非null |
需要注意的是,在if语句中,赋值不能作为顶级表达式。这样会导致编译错误: def var ; if( var = 1 ){} 。除了上述布尔值转换规则以外,Groovy的if/else语句完全一样。三元操作符也被支持,和Java的三元操作符类似。
但是,Groovy优雅的增强了Java中的switch-case语句。其引入了灵活分类器机制——任何实现了 isCase() 方法的对象,都可以作为 case 子句的分类器,而Java中仅仅支持常量作为分类器:
1 2 3 4 5 6 7 8 9 10 |
switch (candidate) { case classifier1 : handle1() ; break case classifier2 : handle2() ; break default : handleDefault() } //上面的语句等价于: if (classifier1.isCase(candidate)) handle1() else if (classifier2.isCase(candidate)) handle2() else handleDefault() |
只要isCase()返回true,那么当前case就匹配。Groovy中下面的类实现了isCase():
类型 | 实现方式 |
Object | a.equals(b) |
Class | a.isInstance(b) |
Collection | a.contains(b) |
Range | a.contains(b) |
Pattern | a.matcher(b.toString()).matches() |
String | (a==null && b==null) || a.equals(b) |
Closure | a.call(b) |
Groovy中没有Java的 do{}while(condition) 或者 for(;;) 循环语法。支持 while(condition) 语法,并且引入 for (variable in iterable) { body } 语法(与Java5引入的新循环语法类似)。此外,基于闭包的循环也很常用:
1 2 3 4 5 6 7 8 9 10 11 12 |
def list = [0, 1, 2, 3, 4] //while循环,注意任何对象都可以作为测试条件 while(list) list.remove( 0 ) //移除直到为空 //for循环 for(item in list) println item //下面的for循环与Java的for(int i = 0;i < 10; i++)类似 for(i in 0..<10) println i //基于闭包的循环 (0..<10).each { println it } |
在Groovy中,文件和类声明的关系不像Java那样的固定。一个Groovy文件可以包含多个public类声明:
- 如果.groovy文件中不包含类声明,它被作为脚本处理。Groovy会生成一个名称与文件名一致的Script类,脚本内容被包含在 run() 方法中,并且新建一个main方法调用之
- 如果.groovy文件中只包含一个类声明,并且起名称与文件名相同,那么和Java一样,具有一一对应关系
- 如果.groovy文件中包含多个不同访问限定符的类,Groovy会分别生成.class
- 如果.groovy文件中混合了脚本和类声明,脚本代码将变成和文件同名的类被执行。此时声明的类不应当和文件同名
1 2 3 |
import javax.lang.* //使用import as可以定义别名,别名可以避免名字冲突,或者避免不合理的名字 import java.lang.AssertionError as AErr |
Groovy的类声明语法和Java很类似,但是需要注意:
- 属性、本地变量在使用前必须声明(注意在Groovy脚本中这不是必须的)
- 支持Java的访问限定符
private protected public ,如果不加限定符:
- 对于成员变量,默认限定符为 private,Groovy会自动生成Getter/Setter方法
- 对于成员方法,默认作为 public 的
- 成员变量或者方法返回值可以声明为动态类型,使用关键字def
- 静态变量或者方法返回值可以声明为动态类型,使用关键字static
- 方法参数的类型是可选的,如果不指定,默认Object
- 方法参数可以提供默认值
- 方法参数可以声明为单个Map,调用时使用命名参数方式传递
下面是一个类定义的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package cc.gmem.study.groovy class Person { def name def age //构造器 Person(name = 'Alex', age = 29) //参数默认值 { this.name = name this.age = age } def listArgs(List l){ } def namedArgs(Map m){ } } |
这里我们使用前面定义的新类:
1 2 3 4 |
def p = new Person() p.namedArgs( key1 : 0, key2 : 1 ) //命名参数方式传参 p.listArgs( [1, 2, 3]) p.listArgs() //任何参数都是可选的 |
在Groovy中,对象属性/方法的访问,可以使用点号导航或者下标语法,甚至,点号导航中可以使用变量:
1 2 |
def methodName = 'listArgs' new Person()."$methodName"()."$methodName"() |
构造Groovy对象,除了使用Java的构造器调用外,还有更多方式:
1 2 3 4 5 6 |
//as关键字进行强制转型列表为对象,列表为构造器入参 Person p = ['Alex', 29] as Person //使用命名参数的方式访问构造器 p = new Person(age : 29, name : 'Alex') //隐式构造 Person p = ['Alex', 29] |
在进行多级点号导航的访问时,一旦路径上的对象为null,就会导致NullPointerException(NPE)的出现。在Java中这需要很繁琐的处理,在入参规格无法确定的情况下,方法实现者可能需要进行多次空指针检查。
Groovy引入特殊的操作符 ?. 来允许安全的属性导航——当路径中出现null时,当前表达式的估算中止,并返回null。下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 |
class Contact{ class Address{ class Tel{ def zoneCode } Tel tel } Address address } def c = new Contact() assert c?.address?.tel?.zoneCode == null |
Groovy完整的支持Java的继承和接口机制,并且两者的代码可以几乎完美的交互。
但是Groovy提供了更加动态的能力,你可以把Map或者Closure强制转型为目标接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface Person { def sayHello(String content); } interface Greeter extends Person { def getName(); } // 对于单方法接口,可以从Closure转型得到 Person p = { content -> println content } as Person // 对于多方法接口,可以从Map转型得到 Greeter g = [ sayHello : { content -> println content }, getName : { 'Alex' } ] as Person // 注意:仅仅调用的方法才需要实现,其它方法不实现也不影响编译 |
trait用于代替旧的Mixin机制,实现(而不是仅仅像接口那样声明)可共享的、可组合的行为,和Java 8 的Default method类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
trait HasId { long id } trait HasVersion { long version } trait Persistent { boolean save() { println "saving ${this.dump()}" } } //trait与接口很类似,但是它可以包含字段、方法实现 trait Entity implements Persistent, HasId, HasVersion { //覆盖父trait的行为 boolean save() { version++ Persistent.super.save() } } //类可以实现trait,就像可以实现interface一样 class Publication implements Entity { String title } class Book extends Publication { String isbn } |
我们知道,Java的重载是静态的——调用哪个重载版本在编译期就确定,依据方法入参的声明类型来决定链接到哪个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class HelloJava { public void m( Object o ) { System.out.println( "Object" ); } public void m( String s ) { System.out.println( "String" ); } public static void main( String[] args ) { HelloJava hj = new HelloJava(); Object o0 = new Object(); Object o1 = new String(); hj.m( o0 ); //Object hj.m( o1 ); //Object } } |
Groovy在这一点上与之完全相反,重载方法的选择是依据变量的运行时类型,动态决定的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class HelloGroovy { def m( Object o ) { System.out.println( "Object" ); } def m( String s ) { System.out.println( "String" ); } static main( String[] args ) { HelloGroovy hg = new HelloGroovy(); Object o0 = new Object(); Object o1 = new String(); hg.m( o0 ); //Object hg.m( o1 ); //String } } |
前面已经提到过,Groovy会默认访问限定符的实例变量自动生成getter/setter。另外需要注意:
- 在类外部, ref.fieldName 会自动映射为getter/setter调用,要想规避这一点,必须使用特殊操作符: ref.@fieldName
- 在类内部, ref.fieldName 直接访问字段
任何类都可以实现 getProperty(name) 和 setProperty(name,value) ,改变Bean属性访问时的默认逻辑。
类似于Xpath,Gpath是Groovy语言强大的对象导航结构,它引入一个展开点操作符: *. 。
对于列表来说: list*.property 等价于 list.collect{ item -> item?.property } ,下面是一段示例代码:
1 2 3 4 5 6 7 8 9 |
println this //当前类:HelloGroovy@14a2f921 println this.class //调用getClass():class HelloGroovy println this.class.methods //调用getClass().getMethod():很长的一个list //*.操作符,对集合的每个元素调用.name println this.class.methods*.name //上述*.操作符可以简写为. println this.class.methods.name println this.class.methods.name.grep(~/get.*/).sort() //[getBinding, getClass, getMetaClass, getProperty] |
可以使用 * 在当前位置把集合展开:
1 2 3 4 5 6 7 8 9 10 11 |
def sum (a,b,c) { a + b + c } assert 6 == sum( *[1, 2, 3]) def range = (1..3) assert [0, 1, 2, 3] == [0, *range] def map = [a:1,b:2] assert [a:1, b:2, c:3] == [c:3, *:map] |
Groovy为Object类添加了一个 use() 方法,用于扩展/修改一个类的可用实例方法。use的入参是一种被称为Category的特殊类,这种类包含一组静态方法,use使得这些方法可以作为第一个入参的实例方法使用。下面是一个简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class StringPlusCategory { static def plus(String self, String operand) { return self.toInteger() + operand.toInteger() } } assert '33' == '3' + '3' //临时改变String的+操作符的行为 use(StringPlusCategory) { assert 6 == '3' + '3' } assert '33' == '3' + '3' |
需要注意的是,use导致的扩展/修改是临时的,不会产生全局性的影响。
如果一个链式调用(chain-of-method)类似: link(producer).to(consumer)
则Groovy允许你将其编写为: link producer to consumer 。这非常类似于自然语言的语法,在DSL(领域特定语言)中非常有用。下面是更多的例子:
1 2 3 |
move(10, forward).painting(color:blue) //支持多参数调用,支持命名参数 move 10, forward painting color:blue |
Groovy还允许把作为最后一个入参的闭包置于括号外面,有时可以达到很奇特的效果:
1 2 3 4 5 6 7 8 9 10 |
def when(expr,closure) { if(expr) closure() } //这里好像我们自己发明了新的控制结构一样 when(true) { println 'OK' ; } |
所谓动态编程,是指在运行时修改对象结构(添加状态、改变行为)的编程风格,是动态语言的优势所在。
当Groovy调用方法时,它不会直接调用,而是总是把调用请求转交给一个中间层——MOP——处理。MOP由相应的API定义。其一系列规则,这些规则限定了:
- Groovy运行时如何处理方法调用请求
- 如何控制中间层
为了寻找正确的目标方法,MOP需要很多信息,这些信息存放在元类中。元类可以在运行时改变或者替换,从而改变对象的行为。
即使不改变元类,Groovy也允许通过一系列钩子方法改变对象的行为。
注解 | 说明 | ||
@groovy.transform.ToString | 生成人类可读的toString()方法 | ||
@groovy.transform.EqualsAndHashCode | 生成equals()和hashCode()方法 | ||
@groovy.transform.TupleConstructor |
生成基于各字段组合的构造函数:
|
||
@groovy.transform.MapConstructor | 生成入参为map的构造函数,键为字段名 | ||
@groovy.transform.Canonical | ToString + EqualsAndHashCode + TupleConstructor | ||
@groovy.transform.InheritConstructors | 生成匹配父类构造器的构造器 | ||
@groovy.lang.Category | 简化Category的编写 | ||
@groovy.transform.IndexedProperty |
为数组、列表字段生成索引化的Getter/Setter:
|
||
@groovy.lang.Lazy |
自动生成字段的延迟初始化逻辑:
|
||
@groovy.lang.Newify |
增加Python、Ruby风格的构造函数:
|
||
@groovy.transform.Sortable | 辅助编写Comparable类 | ||
@groovy.transform.builder.Builder |
生成Fluent API:
|
这些注解用于简化设计模式的实现。
注解 | 说明 | ||
@groovy.transform.BaseScript | 指定脚本需要继承自特定的自定义脚本,而非groovy.lang.Script | ||
@groovy.lang.Delegate |
代理模式:
|
||
@groovy.transform.Immutable | 实现不变模式 | ||
@groovy.transform.TailRecursive | 将尾递归转换为迭代风格,避免栈过深 | ||
@groovy.lang.Singleton |
实现单例模式:
|
注解 | 说明 | ||
@groovy.transform.Synchronized |
实现类似于synchronized的功能,但是可以针对不同对象进行锁定,默认情况下创建$lock、$LOCK(用于静态方法)锁对象
|
||
@groovy.transform.WithReadLock | 读写锁 | ||
@groovy.transform.WithWriteLock |
注解 | 说明 |
@groovy.transform.AutoClone |
辅助实现@java.lang.Cloneable接口,可以通过style自动克隆策略:
|
@groovy.transform.AutoExternalize | 指定外部化包括/排除的字段 |
注解 | 说明 | ||
@groovy.transform.Field |
仅仅在脚本的上下文中有意义,将一个变量声明为脚本类的字段,而非run方法的局部变量
|
||
@groovy.transform.PackageScope | 包内可见的字段,不生成属性 | ||
@groovy.transform.AutoFinal | 提示编译器自动插入final关键字 |
注解 | 说明 | ||||
@Grab |
添加一个依赖:
也可以使用下面的简写法:
|
||||
@GrabResolver |
指定仓库:
|
||||
@GrabConfig |
使用系统类加载器加载:
|
钩子方法 | 说明 | ||
methodMissing |
调用任何对象不支持的方法,转交给该钩子处理:
|
||
propertyMissing |
与上面类似, 当访问不存在的属性时,交由该钩子处理,下面的例子演示了一个DSL风格的二进制计算器:
|
通过把逻辑委托给一个闭包,可以针对一个对象实例(而不是类),在运行时动态修改逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class DynamicPretender { Closure whatToDo = { name -> "accessed $name"} //默认钩子实现 def propertyMissing(String name) { whatToDo(name) } } def one = new DynamicPretender() assert one.hello == 'accessed hello' //修改钩子实现 one.whatToDo = { name -> name.size() } assert one.hello == 5 |
所有由Groovy类都实现了下面的接口(继承了 GroovyObjectSupport ):
1 2 3 4 5 6 7 8 |
public interface GroovyObject { Object invokeMethod(String methodName, Object args); Object getProperty(String propertyName); void setProperty(String propertyName, Object newValue); MetaClass getMetaClass(); void setMetaClass(MetaClass metaClass); } |
任何该接口的子类,都遵守下面的规则:
- 对其属性的访问,委托给getter/setter
- 对任何不存在的方法的调用,委托给invokeMethod()
- 如果子类实现了GroovyInterceptable,那么对任何方法的调用委托给invokeMethod()
缺省适配 GroovyObjectSupport 的方法实现,通过委托调用给对象的元类,应用上述规则:
1 2 3 4 |
public Object invokeMethod(String name, Object args) { return getMetaClass().invokeMethod(this, name, args); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class NoParens { def getProperty(String propertyName) { if (metaClass.hasProperty(this, propertyName)) { return metaClass.getProperty(this, propertyName) } invokeMethod propertyName, null } } class PropUser extends NoParens { boolean existingProperty = true } def user = new PropUser() assert user.existingProperty //现在可以免括号调用0参方法了 assert user.toString() == user.toString |
上面演示的动态编程方式,都是“侵入性”的——需要修改目标类的源码。Groovy还允许修改元类本身,来实现“非侵入性”的动态编程。
Groovy为任何一个类维护一个元类(metaclass)信息,元类使用类型 groovy.lang.MetaClass 表示。元类持有类的所有属性、方法信息,包括Groovy附加的方法。
一般情况下,类的所有实例共享一个元类,但是Groovy支持通过 setMetaClass( metaClass ) 修改单个实例的元类。
你可以很容易的和元类交互,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
MetaClass mc = String.metaClass final Object[] NO_ARGS = [] //检查方法的可用性 assert 1 == mc.respondsTo("toString", NO_ARGS).size() //检查属性、方法的数量 assert 3 == mc.properties.size() assert 74 == mc.methods.size() //检查Groovy动态添加的元方法的数量 assert 176 == mc.metaMethods.size() //调用方法、静态方法、构造器 assert "" == mc.invokeMethod("","toString", NO_ARGS) assert null == mc.invokeStaticMethod(String, "println", NO_ARGS) assert "" == mc.invokeConstructor(NO_ARGS) |
默认情况下, getMetaClass() 通过元类注册表 MetaClassRegistry 来查找元类,除非你覆盖前述方法的实现。该注册表维护了类到元类的映射关系。
Groovy内置了若干元类实现:
- 默认实现 MetaClassImpl ,大部分情况下使用
- ExpandoMetaClass 用于扩展状态和行为
- ProxyMetaClass 装饰一个元类,作为拦截器使用
- 其它内部、测试用的元类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class InspectMe { def method() { } } def tracer = new TracingInterceptor(writer: new StringWriter()) def proxyMetaClass = ProxyMetaClass.getInstance(InspectMe) proxyMetaClass.interceptor = tracer //设置拦截器 InspectMe inspectMe = new InspectMe() inspectMe.metaClass = proxyMetaClass //运行时修改元类 inspectMe.method() println tracer.writer.toString() |
为元类添加状态/行为后,元类自动成为 ExpandoMetaClass ,类的所有实例获得新的状态/行为:
1 2 3 4 5 |
assert String.metaClass =~ /MetaClassImpl/ //为元类添加方法 String.metaClass.low = {-> delegate.toLowerCase() } //闭包的delegate自动指定指向当前对象 assert String.metaClass =~ /ExpandoMetaClass/ //元类已经自动修改 assert "ALEX".low() == "alex" |
使用 Expando 可以在对象级别上动态添加状态/行为:
1 2 3 4 5 6 |
class Person extends Expando {} def person = new Person() person.name = 'Alex' person.sayHello = { -> println 'Hello ' + delegate.name } person.sayHello() |
1 2 3 4 5 6 7 8 9 10 11 12 |
//修改元类,同时添加多个状态/行为 String.metaClass { shift = -1 low {-> delegate.toLowerCase() } } assert "".shift == -1 assert "ALEX".low() == 'alex' //修改元类,添加静态成员 Integer.metaClass.static.answer = {-> 42} assert Integer.answer() == 42 |
Leave a Reply