1. 语法
本章介绍Groovy编程语言的语法。语言的语法源自Java语法,但是使用Groovy的特定构造对其进行了增强,并允许某些简化。
1.1. 注释
1.1.1. 单行注释
// a standalone single line comment
println "hello" // a comment till the end of the line
1.1.2. 多行注释
/* a standalone multiline comment
spanning two lines */
println "hello" /* a multiline comment starting
at the end of a statement */
println 1 /* one */ + 2 /* two */
1.1.3. GroovyDoc注释
与多行注释类似,GroovyDoc注释是多行的,但以 /**
开头,以 */
结尾。第一个GroovyDoc注释行后面的行可以选择以星号 *
开头。这些注释在:
-
类型定义(类,接口,枚举,注解)
-
字段和属性定义
-
方法定义
虽然编译器不会抱怨GroovyDoc注释与上述语言元素没有关联,但是你应该在它之前添加注释。
/**
* A Class description
*/
class Person {
/** the name of the person */
String name
/**
* Creates a greeting method for a certain person.
*
* @param otherPerson the person to greet
* @return a greeting message
*/
String greet(String otherPerson) {
"Hello ${otherPerson}"
}
}
GroovyDoc遵循与Java自己的JavaDoc相同的约定。因此,你将能够使用与JavaDoc相同的标记。
1.1.4. Shebang行
除了单行注释之外,还有一个特殊的行注释,通常称为UNIX系统可以理解的shebang行,它允许脚本直接从命令行运行,前提是你已经安装了Groovy发行版并且 groovy
命令在 PATH
可用。
#!/usr/bin/env groovy
println "Hello from the shebang line"
#
字符必须是文件的第一个字符。任何缩进都会产生编译错误。
1.2. 关键字
以下列表表示Groovy语言的所有关键字:
as |
assert |
break |
case |
catch |
class |
const |
continue |
def |
default |
do |
else |
enum |
extends |
false |
finally |
import |
in |
instanceof |
interface |
new |
null |
package |
return |
super |
switch |
this |
throw |
throws |
trait |
true |
try |
while |
1.3. 标识符
1.3.1. 普通标识符
标识符以字母,美元符号或下划线开头,不能以数字开头;然后后续字符可以包含字母和数字。
一个字母可以在以下范围内:
-
'a' - 'z' (小写ascii字母)
-
'A' - 'Z' (大写ascii字母)
-
'\u00C0' - '\u00D6'
-
'\u00D8' - '\u00F6'
-
'\u00F8' - '\u00FF'
-
'\u0100' - '\uFFFE'
以下是有效标识符的一些示例(此处为变量名称):
def name
def item3
def with_underscore
def $dollarStart
但以下是无效标识符:
def 3tier
def a+b
def a#b
.
后的所有关键字都是有效的标识符:
foo.as
foo.assert
foo.break
foo.case
foo.catch
1.3.2. 引号标识符
引号标识符出现在点表达式的 .
之后。例如,person.name
表达式的 name
部分可以用 person."name"
或 person.'name'
引用。当某些标识符包含Java语言规范禁止但在Groovy允许的非法引用字符时,这一点尤其有趣。例如,破折号,空格,感叹号等字符。
def map = [:]
map."an identifier with a space and double quotes" = "ALLOWED"
map.'with-dash-signs-and-single-quotes' = "ALLOWED"
assert map."an identifier with a space and double quotes" == "ALLOWED"
assert map.'with-dash-signs-and-single-quotes' == "ALLOWED"
正如我们将在下面的字符串部分中看到的,Groovy提供了不同的字符串文字。.
后实际允许所有类型的字符串:
map.'single quote'
map."double quote"
map.'''triple single quote'''
map."""triple double quote"""
map./slashy string/
map.$/dollar slashy string/$
普通字符串和Groovy的GStrings(插值字符串)之间存在差异,因为在后一种情况下,插值将插入到最终字符串中以评估整个标识符的值:
def firstname = "Homer"
map."Simpson-${firstname}" = "Homer Simpson"
assert map.'Simpson-Homer' == "Homer Simpson"
1.4. 字符串
Groovy允许你实例化 java.lang.String
对象,以及GStrings(groovy.lang.GString
),它们在其他编程语言中也被称为插值字符串。
1.4.1. 单引号字符串
单引号字符串是由单引号括起来的一系列字符:
'a single quoted string'
单引号字符串是普通的 java.lang.String
,不支持插值。
1.4.2. 字符串连接
所有Groovy字符串都可以用 +
运算符连接:
assert 'ab' == 'a' + 'b'
1.4.3. 三重单引号字符串
'''a triple single quoted string'''
三重单引号字符串是普通的 java.lang.String
,不支持插值。
三重单引号字符串是多行的。你可以跨越行边界跨字符串的内容,而无需将字符串拆分为多个部分,不再需要连接或换行字符:
如果你的代码是缩进的,例如在类方法体中,则字符串将包含缩进的空格。Groovy Development Kit包含使用 String#stripIndent()
方法剥离缩进的方法,以及 String#stripMargin()
方法使用分隔符来标识要从字符串开头删除的文本。
创建如下字符串时:
def startingAndEndingWithANewline = '''
line one
line two
line three
'''
你会注意到结果字符串包含换行符作为第一个字符。可以通过使用反斜杠转义换行来剥离该字符:
def strippedFirstNewline = '''\
line one
line two
line three
'''
assert !strippedFirstNewline.startsWith('\n')
转义特殊字符
你可以使用反斜杠字符转义单引号,以避免终止字符串文字:
'an escaped single quote: \' needs a backslash'
你可以使用双反斜杠来转义转义字符本身:
'an escaped escape character: \\ needs a double backslash'
一些特殊字符也使用反斜杠作为转义字符:
转义序列 |
字符 |
'\t' |
制表符 |
'\b' |
退格符 |
'\n' |
换行符 |
'\r' |
回车符 |
'\f' |
换页符 |
'\\' |
反斜线 |
'\'' |
单引号(单引号和三重单引号字符串) |
'\"' |
双引号(双引号和三重双引号字符串) |
Unicode转义序列
对于键盘上不存在的字符,可以使用unicode转义序列:一个反斜杠,后跟’u',然后是4个十六进制数字。
例如,欧元货币符号可以表示为:
'The Euro currency symbol: \u20AC'
1.4.4. 双引号字符串
双引号字符串是由双引号括起来的一系列字符:
"a double quoted string"
如果没有插值表达式,双引号字符串是普通的 java.lang.String
,但如果存在插值则是 groovy.lang.GString
实例。
要转义双引号,可以使用反斜杠字符:"A double quote: \""。
字符串插值
除了单引号和三重单引号字符串之外,任何Groovy表达式都可以在所有字符串文字中进行插值。插值是在对字符串求值时将字符串中的占位符替换为其值的行为。占位符表达式由 ${}
包围,或者以 $
为前缀的点表达式。当GString被传递给以String为参数的方法时,通过调用该表达式上的 toString()
,将占位符内的表达式值计算为其字符串表示形式。
这里有一个字符串,其占位符引用局部变量:
def name = 'Guillaume' // a plain string
def greeting = "Hello ${name}"
assert greeting.toString() == 'Hello Guillaume'
任何Groovy表达式都是有效的,正如我们在本例中可以看到的算术表达式:
def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'
不仅在 ${}
占位符之间允许表达式,语句也可以。但是,语句的值只是 null
。因此,如果在该占位符中插入了多个语句,则最后一个语句应以某种方式返回要插入的有意义值。例如,"The sum of 1 and 2 is equal to ${def a = 1; def b = 2; a + b}" 是支持的并且按预期工作但是一个好的做法通常是坚持在GString占位符中使用简单表达式。
除了 ${}
占位符之外,我们还可以使用一个单独的 $
符号作为点表达式的前缀:
def person = [name: 'Guillaume', age: 36]
assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'
但只有 a.b
,a.b.c
等形式的点表达式才有效,包含括号的表达式(如方法调用,闭包的花括号或算术运算符)将无效。给定以下数字的变量定义:
def number = 3.14
以下语句将抛出 groovy.lang.MissingPropertyException
,因为Groovy认为你正在尝试访问该数字的 toString
方法,但该方法不存在:
shouldFail(MissingPropertyException) {
println "$number.toString()"
}
如果你需要在GString中转义 $
或 ${}
占位符,使它们看起来没有插值,你只需要使用 \
反斜杠字符来转义美元符号即可:
assert '${name}' == "\${name}"
内插闭包表达式的特例
到目前为止,我们已经看到我们可以在 ${}
占位符内插入任意表达式,但是闭包表达式有一个特殊的情况和符号。当占位符包含箭头 ${→}
时,表达式实际上是一个闭包表达式 -
你可以将其视为一个闭包,前面有一个美元符号:
def sParameterLessClosure = "1 + 2 == ${-> 3}" (1)
assert sParameterLessClosure == '1 + 2 == 3'
def sOneParamClosure = "1 + 2 == ${ w -> w << 3}" (2)
assert sOneParamClosure == '1 + 2 == 3'
1 | 该闭包是一个无参数的闭包,它不带参数。 |
2 | 该闭包采用一个 java.io.StringWriter 参数,你可以使用 << 左移运算符向其追加内容。在任何一种情况下,两个占位符都是嵌入式闭包。 |
从外观上看,它看起来像是一种定义要插入的表达式的更冗长的方式,但是闭包与单纯的表达式相比具有一个有趣的优势:惰性评估。
让我们考虑以下示例:
def number = 1 (1)
def eagerGString = "value == ${number}"
def lazyGString = "value == ${ -> number }"
assert eagerGString == "value == 1" (2)
assert lazyGString == "value == 1" (3)
number = 2 (4)
assert eagerGString == "value == 1" (5)
assert lazyGString == "value == 2" (6)
1 | 我们定义一个包含 1 的 number 变量,然后我们在两个GStrings中插入,作为 eagerGString 中的表达式和 lazyGString 中的闭包。 |
2 | 我们希望结果字符串包含 eagerGString 的相同字符串值 1 。 |
3 | 对于 lazyGString 也是如此。 |
4 | 然后我们将变量的值更改为新数字。 |
5 | 使用简单的插值表达式,该值实际上是在创建GString时绑定的。 |
6 | 但是使用闭包表达式时,会在每次将GString强制转换为String时调用闭包,从而生成包含新数字值的更新字符串。 |
带有多个参数的嵌入式闭包表达式将在运行时生成异常。仅允许具有零个或一个参数的闭包。
与Java的互操作性
当一个方法(无论是用Java还是Groovy实现)需要 java.lang.String
,但我们传递一个 groovy.lang.GString
实例时,GString的 toString()
方法会被自动且透明地调用。
String takeString(String message) { (4)
assert message instanceof String (5)
return message
}
def message = "The message is ${'hello'}" (1)
assert message instanceof GString (2)
def result = takeString(message) (3)
assert result instanceof String
assert result == 'The message is hello'
1 | 我们创建一个GString变量。 |
2 | 我们仔细检查它是GString的一个实例。 |
3 | 然后我们将该GString传递给一个以String作为参数的方法。 |
4 | takeString() 方法的签名显式地表明它的唯一参数是 String 。 |
5 | 我们还验证参数确实是 String 而不是 GString 。 |
GString和String的哈希码
尽管可以使用插值字符串代替普通Java字符串,但它们与字符串的不同之处在于:它们的hashCodes不同。普通Java字符串是不可变的,而GString的结果字符串表示形式可能会有所不同,具体取决于其内插值。即使对于相同的结果字符串,GStrings和Strings也没有相同的hashCode。
assert "one: ${1}".hashCode() != "one: 1".hashCode()
应该避免使用GString作为Map的键,特别是当我们试图检索与String而不是GString关联的值时。
def key = "a"
def m = ["${key}": "letter ${key}"] (1)
assert m["a"] == null (2)
1 | 使用初始键值对创建映射,其键是GString |
2 | 当我们尝试使用String键获取值时,我们将找不到它,因为Strings和GString具有不同的hashCode值 |
1.4.5. 三重双引号字符串
三重双引号字符串的行为类似于双引号字符串,只是它们是多行的,就像三重单引号字符串一样。
def name = 'Groovy'
def template = """
Dear Mr ${name},
You're the winner of the lottery!
Yours sincerly,
Dave
"""
assert template.toString().contains('Groovy')
双引号和单引号都不需要在三重双引号字符串中进行转义。
1.4.6. Slashy字符串
除了通常引用的字符串之外,Groovy还提供了使用 /
作为分隔符的字符串。Slashy字符串对于定义正则表达式和模式特别有用,因为不需要转义反斜杠。
一个slashy字符串的示例:
def fooPattern = /.*foo.*/
assert fooPattern == '.*foo.*'
只需使用反斜杠转义正斜杠:
def escapeSlash = /The character \/ is a forward slash/
assert escapeSlash == 'The character / is a forward slash'
Slashy字符串可以是多行的:
def multilineSlashy = /one
two
three/
assert multilineSlashy.contains('\n')
Slashy字符串也可以插值(即GString):
def color = 'blue'
def interpolatedSlashy = /a ${color} car/
assert interpolatedSlashy == 'a blue car'
有一些问题需要注意。
空的slashy字符串不能用双正斜杠表示,因为Groovy解析器将其理解为行注释。这就是为什么以下断言实际上不会编译,因为它看起来像一个未完成语句:
assert '' == //
由于slashy字符串的设计主要是为了简化regexp,所以像 $()
这样的GStrings中的一些错误也适用于slashy字符串。
1.4.7. 美元slashy字符串
美元slashy字符串是多行GStrings,以 $/
开头和以 /$
结束。转义字符是美元符号,它可以转义另一个美元或正斜杠。但是,美元或正斜杠都不需要被转义,除非你要转义字符串子序列中的美元符号,该子序列以GString占位符序列开头,或者如果你需要转义序列,该子序列将以一个美元slashy字符串结束分隔符 /$
开始。
这是一个例子:
def name = "Guillaume"
def date = "April, 1st"
def dollarSlashy = $/
Hello $name,
today we're ${date}.
$ dollar sign
$$ escaped dollar sign
\ backslash
/ forward slash
$/ escaped forward slash
$$$/ escaped opening dollar slashy
$/$$ escaped closing dollar slashy
/$
assert [
'Guillaume',
'April, 1st',
'$ dollar sign',
'$ escaped dollar sign',
'\\ backslash',
'/ forward slash',
'/ escaped forward slash',
'$/ escaped opening dollar slashy',
'/$ escaped closing dollar slashy'
].every { dollarSlashy.contains(it) }
1.4.8. 字符串总结
字符串名称 |
字符串语法 |
可插值 |
可多行 |
转义字符 |
单引号 |
|
|
||
三重单引号 |
|
|
|
|
双引号 |
|
|
|
|
三重双引号 |
|
|
|
|
Slashy |
|
|
|
|
美元Slashy |
|
|
|
|
1.4.9. 字符
与Java不同,Groovy没有明确的字符文字。但是,你可以通过三种不同的方式明确地将Groovy字符串设置为字符:
char c1 = 'A' (1)
assert c1 instanceof Character
def c2 = 'B' as char (2)
assert c2 instanceof Character
def c3 = (char)'C' (3)
assert c3 instanceof Character
1 | 通过指定 char 类型来显式声明包含字符的变量 |
2 | 通过使用 as 运算符类型强转 |
3 | 通过使用强制转换为 char 的操作 |
当字符保存在变量中时,选项一很有趣,而当必须将char值作为方法调用的参数传递时,其他两个选项更有趣。
1.5. 数字
Groovy支持不同类型的整数和浮点数,由通常的Java类型 Number
支持。
1.5.1. 整数
整数类型与Java中相同:
-
byte
-
char
-
short
-
int
-
long
-
java.lang.BigInteger
你可以使用以下声明创建这些类型的整数:
// primitive types
byte b = 1
char c = 2
short s = 3
int i = 4
long l = 5
// infinite precision
BigInteger bi = 6
如果用 def
关键字使用可选类型,则整数的类型将有所不同:它会适应能容纳这个数字的包装类型。
对于正数:
def a = 1
assert a instanceof Integer
// Integer.MAX_VALUE
def b = 2147483647
assert b instanceof Integer
// Integer.MAX_VALUE + 1
def c = 2147483648
assert c instanceof Long
// Long.MAX_VALUE
def d = 9223372036854775807
assert d instanceof Long
// Long.MAX_VALUE + 1
def e = 9223372036854775808
assert e instanceof BigInteger
以及负数:
def na = -1
assert na instanceof Integer
// Integer.MIN_VALUE
def nb = -2147483648
assert nb instanceof Integer
// Integer.MIN_VALUE - 1
def nc = -2147483649
assert nc instanceof Long
// Long.MIN_VALUE
def nd = -9223372036854775808
assert nd instanceof Long
// Long.MIN_VALUE - 1
def ne = -9223372036854775809
assert ne instanceof BigInteger
可选的非十进制表示
数字也可以用二进制,八进制,十六进制和十进制数表示。
二进制数
二进制数字以 0b
前缀开头:
int xInt = 0b10101111
assert xInt == 175
short xShort = 0b11001001
assert xShort == 201 as short
byte xByte = 0b11
assert xByte == 3 as byte
long xLong = 0b101101101101
assert xLong == 2925l
BigInteger xBigInteger = 0b111100100001
assert xBigInteger == 3873g
int xNegativeInt = -0b10101111
assert xNegativeInt == -175
八进制数
八进制数以 0
前缀开头,后跟八进制数字。
int xInt = 077
assert xInt == 63
short xShort = 011
assert xShort == 9 as short
byte xByte = 032
assert xByte == 26 as byte
long xLong = 0246
assert xLong == 166l
BigInteger xBigInteger = 01111
assert xBigInteger == 585g
int xNegativeInt = -077
assert xNegativeInt == -63
十六进制数
十六进制数以 0x
前缀开头,后跟十六进制数字。
int xInt = 0x77
assert xInt == 119
short xShort = 0xaa
assert xShort == 170 as short
byte xByte = 0x3a
assert xByte == 58 as byte
long xLong = 0xffff
assert xLong == 65535l
BigInteger xBigInteger = 0xaaaa
assert xBigInteger == 43690g
Double xDouble = new Double('0x1.0p0')
assert xDouble == 1.0d
int xNegativeInt = -0x77
assert xNegativeInt == -119
1.5.2. 小数
小数类型与Java中的相同:
-
float
-
double
-
java.lang.BigDecimal
你可以使用以下声明创建这些类型的小数:
// primitive types
float f = 1.234
double d = 2.345
// infinite precision
BigDecimal bd = 3.456
小数可以使用指数,e
或 E
指数字母,后跟可选符号,以及表示指数的整数:
assert 1e3 == 1_000.0
assert 2E4 == 20_000.0
assert 3e+1 == 30.0
assert 4E-2 == 0.04
assert 5e-1 == 0.5
为了方便地进行精确的小数计算,Groovy选择 java.lang.BigDecimal
作为其小数类型。此外,支持 float
和 double
,但需要显式类型声明,类型强转或类型后缀。即使 BigDecimal
是小数的默认值,也可以在 float
或 double
作为参数类型的方法或闭包中接受这样的数字。
小数不能使用二进制,八进制或十六进制表示来表示。
1.5.3. 数字下划线
在编写长数字字面量时,很难弄清楚某些数字是如何组合在一起的,例如成千上万的单词组等。通过允许你在数字字面量中放置下划线,可以更容易地发现这些组:
long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010
1.5.4. 数字类型后缀
我们可以通过给定后缀(见下表,大小写均可以)来强制一个数字(包括二进制,八进制和十六进制)具有特定类型。
类型 |
后缀 |
BigInteger |
|
Long |
|
Integer |
|
BigDecimal |
|
Double |
|
Float |
|
例子:
assert 42I == new Integer('42')
assert 42i == new Integer('42') // lowercase i more readable
assert 123L == new Long("123") // uppercase L more readable
assert 2147483648 == new Long('2147483648') // Long type used, value too large for an Integer
assert 456G == new BigInteger('456')
assert 456g == new BigInteger('456')
assert 123.45 == new BigDecimal('123.45') // default BigDecimal type used
assert 1.200065D == new Double('1.200065')
assert 1.234F == new Float('1.234')
assert 1.23E23D == new Double('1.23E23')
assert 0b1111L.class == Long // binary
assert 0xFFi.class == Integer // hexadecimal
assert 034G.class == BigInteger // octal
1.5.5. 数学运算
虽然稍后会介绍运算符,但讨论数学运算的行为以及它们的结果类型是很重要的。
除法和幂次运算除外(如下所述):
-
byte
,char
,short
和int
相互之间的二进制操作结果是int
-
涉及
long
和byte
,char
,short
,int
的二进制操作结果都是long
-
涉及
BigInteger
和任何其他整数类型的二进制运算结果都是BigInteger
-
涉及
BigDecimal
和byte
,char
,short
,int
和BigInteger
的二进制操作结果为BigDecimal
-
float
,double
和BigDecimal
之间的二进制运算操作结果是double
-
两个
BigDecimal
之间的二进制运算操作结果是BigDecimal
下表总结了这些规则:
byte |
char |
short |
int |
long |
BigInteger |
float |
double |
BigDecimal |
|
byte |
int |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
char |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|
short |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
||
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|||
long |
long |
BigInteger |
double |
double |
BigDecimal |
||||
BigInteger |
BigInteger |
double |
double |
BigDecimal |
|||||
float |
double |
double |
double |
||||||
double |
double |
double |
|||||||
BigDecimal |
BigDecimal |
由于Groovy的运算符重载,通常的算术运算符也适用于 BigInteger
和 BigDecimal
,与Java不同,在Java中必须使用显式方法来操作这些数字。
除法运算符的情况
如果操作数是 float
或 double
,则除法运算符 /
(和 /=
用于除法和赋值)产生 double
结果,否则产生 BigDecimal
结果(当两个操作数都是整数类型 short
,char
,byte
, int
,long
,BigInteger
或 BigDecimal
的任意组合时)。
如果除法是精确的,则使用 divide()
方法执行 BigDecimal
除法(即产生可以在相同精度和比例的范围内表示的结果),或者使用 MathContext
,其精度为两个操作数的最大 精度 加上10的额外精度,以及最大值为10的 刻度 和操作数刻度的最大值。
精度:所有数字的个数,刻度:小数点后面的数字个数。
对于像Java中的整数除法,你应该使用 intdiv() 方法,因为Groovy不提供专用的整数除法运算符符号。
|
幂次运算符的情况
幂运算由 **
运算符表示,有两个参数:基数和指数。幂操作的结果取决于其操作数和操作的结果(特别是如果结果可以表示为整数值)。
Groovy的幂操作使用以下规则来确定结果类型:
-
如果指数是一个小数值
-
如果结果可以表示为
Integer
,则返回一个Integer
-
否则,如果结果可以表示为
Long
,则返回Long
-
否则返回
Double
-
-
如果指数是一个整数值
-
如果指数严格为负,则如果结果值适合该类型,则返回
Integer
,Long
或Double
-
如果指数是正数或零
-
如果基数是
BigDecimal
,则返回BigDecimal
结果值 -
如果基数是
BigInteger
,则返回BigInteger
结果值 -
如果基数是
Integer
,那么如果结果值适合它则返回Integer
,否则返回BigInteger
-
如果基数是
Long
,那么如果结果值适合它则返回Long
,否则返回BigInteger
-
-
我们可以用几个例子说明这些规则:
// base和exponent是整数,结果可以用Integer表示
assert 2 ** 3 instanceof Integer // 8
assert 10 ** 9 instanceof Integer // 1_000_000_000
// base是long,所以结果用Long表示(尽管它可能适合Integer)
assert 5L ** 2 instanceof Long // 25
// 结果不能表示为Integer或Long,因此返回BigInteger
assert 100 ** 10 instanceof BigInteger // 10e20
assert 1234 ** 123 instanceof BigInteger // 170515806212727042875...
// base是BigDecimal,指数是负int,但结果可以表示为Integer
assert 0.5 ** -2 instanceof Integer // 4
// base是int,而exponent是一个负float,但同样结果可以表示为Integer
assert 1 ** -0.3f instanceof Integer // 1
// base是int,而exponent是一个负int,但结果表示为Double(基数和指数实际上都转换为double)
assert 10 ** -1 instanceof Double // 0.1
// base是BigDecimal,exponent是int,所以返回一个BigDecimal
assert 1.2 ** 10 instanceof BigDecimal // 6.1917364224
// base是float或double,exponent是int,但结果只能表示为Double
assert 3.4f ** 5 instanceof Double // 454.35430372146965
assert 5.6d ** 2 instanceof Double // 31.359999999999996
// 指数是小数值,结果只能表示为Double
assert 7.8 ** 1.9 instanceof Double // 49.542708423868476
assert 2 ** 0.1f instanceof Double // 1.0717734636432956
1.6. 布尔
Boolean是一种特殊的数据类型,用于表示真值:true
和 false
。将此数据类型用于跟踪真/假条件的简单标志。
布尔值可以存储在变量中,赋值到字段中,就像任何其他数据类型一样:
def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true
true
和 false
是唯一两个原始布尔值。但是更复杂的布尔表达式可以使用逻辑运算符表示。
此外,Groovy具有 特殊规则(通常称为Groovy Truth),用于将非布尔对象强制转换为布尔值。
1.7. 列表
Groovy使用逗号分隔的值列表(用方括号括起来)来表示列表。Groovy列表是普通的JDK java.util.List
,因为Groovy没有定义自己的集合类。默认情况下,定义列表时使用的具体列表实现是 java.util.ArrayList
,除非你决定另行指定,我们将在后面看到。
def numbers = [1, 2, 3] (1)
assert numbers instanceof List (2)
assert numbers.size() == 3 (3)
1 | 我们定义一个用逗号分隔的列表编号,并用方括号括起来,然后我们将该列表分配给一个变量 |
2 | 该列表是Java的 java.util.List 接口的实例 |
3 | 可以使用 size() 方法查询列表的大小,并显示我们的列表包含3个元素 |
在上面的示例中,我们使用了同类列表,但你也可以创建包含异构类型值的列表:
def heterogeneous = [1, "a", true] (1)
1 | 这里的列表包含数字,字符串和布尔值 |
我们提到默认情况下,列表实际上是 java.util.ArrayList
的实例,但是可以为我们的列表使用不同的支持类型,这要归功于使用 as
类型强制操作符,或者使用变量的显式类型声明:
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList
def linkedList = [2, 3, 4] as LinkedList (1)
assert linkedList instanceof java.util.LinkedList
LinkedList otherLinked = [3, 4, 5] (2)
assert otherLinked instanceof java.util.LinkedList
1 | 使用 as 类型强制操作符来显式声明是 java.util.LinkedList 类型 |
2 | 使用变量的显式类型声明是 java.util.LinkedList 类型 |
你可以使用带有正索引的 []
下标运算符(用于读取和设置值)访问列表元素,或负索引访问列表末尾的元素以及范围切片,并使用 <<
左移运算符将元素附加到列表:
def letters = ['a', 'b', 'c', 'd']
assert letters[0] == 'a' (1)
assert letters[1] == 'b'
assert letters[-1] == 'd' (2)
assert letters[-2] == 'c'
letters[2] = 'C' (3)
assert letters[2] == 'C'
letters << 'e' (4)
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
assert letters[1, 3] == ['b', 'd'] (5)
assert letters[2..4] == ['C', 'd', 'e'] (6)
1 | 访问列表的第一个元素(从零开始计数) |
2 | 使用负索引访问列表的最后一个元素:-1是列表末尾的第一个元素 |
3 | 使用赋值为列表的第三个元素设置新值 |
4 | 使用 << 左移运算符在列表的末尾追加一个元素 |
5 | 一次访问两个元素,返回包含这两个元素的新列表 |
6 | 使用范围从列表中访问从开始到结束元素位置的一系列值 |
由于列表本质上可以是异构的,因此列表还可以包含其他列表来创建多维列表:
def multi = [[0, 1], [2, 3]] (1)
assert multi[1][0] == 2 (2)
1 | 定义数字列表的列表 |
2 | 访问第二个列表的第一个元素 |
1.8. 数组
Groovy的数组重用了列表表示法,但是为了制作这样的数组,你需要通过强转或类型声明来明确地定义数组的类型。
String[] arrStr = ['Ananas', 'Banana', 'Kiwi'] (1)
assert arrStr instanceof String[] (2)
assert !(arrStr instanceof List)
def numArr = [1, 2, 3] as int[] (3)
assert numArr instanceof int[] (4)
assert numArr.size() == 3
1 | 使用显式变量类型声明定义字符串数组 |
2 | 断言我们创建了一个字符串数组 |
3 | 使用 as 运算符创建一个int数组 |
4 | 断言我们创建了一个int数组 |
你还可以创建多维数组:
def matrix3 = new Integer[3][3] (1)
assert matrix3.size() == 3
Integer[][] matrix2 (2)
matrix2 = [[1, 2], [3, 4]]
assert matrix2 instanceof Integer[][]
1 | 你可以定义新数组的边界 |
2 | 或者声明一个数组而不指定其边界 |
对数组元素的访问遵循与列表相同的表示法:
String[] names = ['Cédric', 'Guillaume', 'Jochen', 'Paul']
assert names[0] == 'Cédric' (1)
names[2] = 'Blackdrag' (2)
assert names[2] == 'Blackdrag'
1 | 检索数组的第一个元素 |
2 | 将数组的第三个元素的值设置为新值 |
Groovy不支持Java的数组初始化表示法,因为花括号可能会被Groovy误解为闭包的符号。
1.9. 映射
有时在其他语言中称为字典或关联数组,Groovy具有映射功能。映射将键与值相关联,使用冒号分隔键和值,使用逗号分隔每个键/值对,以及用方括号括起的整个键和值。
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'] (1)
assert colors['red'] == '#FF0000' (2)
assert colors.green == '#00FF00' (3)
colors['pink'] = '#FF00FF' (4)
colors.yellow = '#FFFF00' (5)
assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'
1 | 我们定义了一个字符串颜色名称的映射,与其十六进制编码的html颜色相关联 |
2 | 我们使用下标表示法来检查与 red 键相关联的内容 |
3 | 也可以使用属性表示法来断言 green 颜色的十六进制表示 |
4 | 同样,我们可以使用下标符号来添加新的键/值对 |
5 | 或者属性表示法,添加 yellow 键/值对 |
当使用键的名称时,我们实际上在映射中定义了 String
键。Groovy创建的映射实际上是 java.util.LinkedHashMap
的实例。
如果你尝试访问映射中不存在的键:
assert colors.unknown == null
你将检索到 null
结果。
在上面的示例中,我们使用了字符串键,但你也可以使用其他类型的值作为键:
def numbers = [1: 'one', 2: 'two']
assert numbers[1] == 'one'
在这里,我们使用数字作为键,因为数字可以明确地被识别为数字,因此Groovy不会像我们之前的示例中那样创建字符串键。但是考虑一下你要传递一个变量代替键的情况,让该变量的值成为键:
def key = 'name'
def person = [key: 'Guillaume'] (1)
assert !person.containsKey('name') (2)
assert person.containsKey('key') (3)
1 | 与 'Guillaume' 名称关联的 key 实际上是 "key" 字符串,而不是与 key 变量关联的值 |
2 | 映射不包含 'name' 键 |
3 | 相反,映射包含 'key' 键 |
你还可以传递带引号的字符串以及键:["name": "Guillaume"]。如果你的键字符串不是有效的标识符,那么这是必需的,例如,如果你想创建一个包含哈希的字符串键,如:["street-name": "Main street"]。
当你需要在映射定义中将变量值作为键传递时,必须用括号括起变量或表达式:
person = [(key): 'Guillaume'] (1)
assert person.containsKey('name') (2)
assert !person.containsKey('key') (3)
1 | 这一次,我们用括号括起键变量,指示解析器传递变量而不是定义字符串键 |
2 | 映射确实包含 name 键 |
3 | 但是映射不像以前那样包含 key 键 |
2. 运算符
本章介绍Groovy编程语言的运算符。
2.1. 算术运算符
Groovy支持你在数学和其他编程语言(如Java)中找到的常用算术运算符。支持所有Java算术运算符。我们将在以下示例中介绍它们。
2.1.1. 普通算术运算符
Groovy中提供了以下二元算术运算符:
运算符 |
用途 |
备注 |
|
加 |
|
|
减 |
|
|
乘 |
|
|
除 |
使用 |
|
取余 |
|
|
求幂 |
有关操作返回类型的更多信息,请参阅有关 幂次运算 的部分。 |
以下是这些运算符的一些使用示例:
assert 1 + 2 == 3
assert 4 - 3 == 1
assert 3 * 5 == 15
assert 3 / 2 == 1.5
assert 10 % 3 == 1
assert 2 ** 3 == 8
2.1.2. 一元运算符
+
和 -
运算符也可作为一元运算符使用:
assert +3 == 3
assert -4 == 0 - 4
assert -(-1) == 1 (1)
1 | 请注意使用括号括起表达式用以将一元减号应用于该被包围表达式。 |
就一元算术运算符而言,++
(自增)和 --
(自减)运算符在前缀和后缀表示法中均可用:
def a = 2
def b = a++ * 3 (1)
assert a == 3 && b == 6
def c = 3
def d = c-- * 2 (2)
assert c == 2 && d == 6
def e = 1
def f = ++e + 3 (3)
assert e == 2 && f == 5
def g = 4
def h = --g + 1 (4)
assert g == 3 && h == 4
1 | 先计算表达式并赋值给 b 后,再增加 a |
2 | 先计算表达式并赋值给 d 后,再减少 c |
3 | 先增加 e ,再计算表达式并赋值给 f |
4 | 先减少 g ,再计算表达式并赋值给 h |
2.1.3. 赋值算术运算符
我们在上面看到的二元算术运算符也可以在其赋值形式中使用:
-
+=
-
-=
-
*=
-
/=
-
%=
-
**=
让我们看看他们的行为:
def a = 4
a += 3
assert a == 7
def b = 5
b -= 3
assert b == 2
def c = 5
c *= 3
assert c == 15
def d = 10
d /= 2
assert d == 5
def e = 10
e %= 3
assert e == 1
def f = 3
f **= 2
assert f == 9
2.2. 关系运算符
关系运算符允许对象之间的比较,以知道两个对象是相同还是不同,或者一个对象是否大于,小于或等于另一个。
以下运算符可用:
运算符 |
用途 |
|
相等 |
|
不等 |
|
小于 |
|
小于或等于 |
|
大于 |
|
大于或等于 |
以下是使用这些运算符进行简单数字比较的一些示例:
assert 1 + 2 == 3
assert 3 != 4
assert -2 < 3
assert 2 <= 2
assert 3 <= 4
assert 5 > 1
assert 5 >= -2
2.3. 逻辑运算符
Groovy为布尔表达式提供了三个逻辑运算符:
-
&&
: 逻辑"与" -
||
: 逻辑"或" -
!
: 逻辑"非"
让我们用以下例子来说明它们:
assert !false (1)
assert true && true (2)
assert true || false (3)
1 | false的非是真 |
2 | true与true是真 |
3 | true或false是真 |
2.3.1. 优先级
逻辑“非”具有比逻辑“与”更高的优先级。
assert (!false && false) == false (1)
1 | 这里断言是真的(因为括号中的表达式是假的),因为“非”的优先级高于“与”,所以“非”只适用于第一个“false”术语;否则,它将应用于“与”后的结果,将其变为真,并且断言将失败 |
逻辑“与”具有比逻辑“或”更高的优先级。
assert true || true && false (1)
1 | 这里断言是真的,因为“与”的优先级高于“或”,因此“或”最后执行并返回true;否则,“与”将执行最后并返回false,并且断言将失败 |
2.3.2. 短路
逻辑 ||
运算符支持短路:如果左操作数为真,则它知道结果在任何情况下都为真,因此它不会计算右操作数。仅当左操作数为false时,才会评估右操作数。
同样对于逻辑 &&
运算符:如果左操作数为false,则它知道结果在任何情况下都将为false,因此它不会计算右操作数。仅当左操作数为真时才会评估右操作数。
boolean checkIfCalled() { (1)
called = true
}
called = false
true || checkIfCalled()
assert !called (2)
called = false
false || checkIfCalled()
assert called (3)
called = false
false && checkIfCalled()
assert !called (4)
called = false
true && checkIfCalled()
assert called (5)
1 | 我们创建了一个函数,无论何时调用它,都会将被 called 标志设置为true |
2 | 在该情况下,我们确认如果 || 运算符的左操作数为真,则不调用该函数,|| 会短路右操作数的评估 |
3 | 在该情况下,左操作数为false,因此调用该函数,我们的标志现在被设置为真 |
4 | 同样对于 && ,我们确认函数没有被调用,因为左操作数为假 |
5 | 但是左操作数为真时调用了该函数 |
2.4. 位运算符
Groovy提供4个按位运算符:
-
&
:按位"与" -
|
:按位"或" -
^
:按位"异或" -
~
:按位"取反"
按位运算符可以应用于 byte
或 int
并返回 int
:
int a = 0b00101010
assert a == 42
int b = 0b00001000
assert b == 8
assert (a & a) == a (1)
assert (a & b) == b (2)
assert (a | a) == a (3)
assert (a | b) == a (4)
int mask = 0b11111111 (5)
assert ((a ^ a) & mask) == 0b00000000 (6)
assert ((a ^ b) & mask) == 0b00100010 (7)
assert ((~a) & mask) == 0b11010101 (8)
1 | 按位与 |
2 | 按位与,并返回公共位 |
3 | 按位或 |
4 | 按位或,并返回所有 '1' 位 |
5 | 设置掩码以仅检查最后8位 |
6 | 自身按位异或返回0 |
7 | 按位异或 |
8 | 按位取反 |
值得注意的是,基本类型的内部表示遵循Java 语言规范。特别是,原始类型是有符号的,这意味着对于按位取反,使用掩码仅检索必要的位总是好的。
在Groovy中,按位运算符具有可重载的特性,这意味着你可以为任何类型的对象定义这些运算符的行为。
2.5. 条件运算符
2.5.1. 否定运算符
“否定”运算符用感叹号(!)表示,并反转底层布尔表达式的结果。特别是,可以将否定运算符与Groovy-Truth结合起来:
assert (!true) == false (1)
assert (!'foo') == false (2)
assert (!'') == true (3)
1 | true 的否定是 false |
2 | 'foo’是一个非空字符串,计算结果为 true ,因此否定返回 false |
3 | ''是一个空字符串,计算结果为 false ,因此否定返回 true |
2.5.2. 三元运算符
三元运算符是一个快捷表达式,相当于if/else分支为某变量赋值:
if (string!=null && string.length()>0) {
result = 'Found'
} else {
result = 'Not found'
}
你可以写:
result = (string!=null && string.length()>0) ? 'Found' : 'Not found'
三元运算符也与Groovy-Truth兼容,因此你可以使其更简单:
result = string ? 'Found' : 'Not found'
2.5.3. Elvis运算符
“Elvis运算符”是三元运算符的简写。这方面的一个实例是,如果表达式解析为 false
-ish(如Groovy-Truth中),则返回“合理的默认值”。一个简单的例子可能如下所示:
displayName = user.name ? user.name : 'Anonymous' (1)
displayName = user.name ?: 'Anonymous' (2)
1 | 使用三元运算符,你必须重复要赋值的值 |
2 | 使用Elvis运算符,如果不是 false -ish,则使用已测试的值 |
使用Elvis运算符可以减少代码的详细程度,并消除复制条件和真返回值中测试的表达式的需要,减少重构时出错的风险。
2.6. 对象运算符
2.6.1. 安全导航运算符
安全导航运算符用于避免 NullPointerException
。通常,在引用对象时,可能需要在访问对象的方法或属性之前验证它是否为 null
。为了避免这种情况,安全导航运算符将只返回 null
而不是抛出异常,如下所示:
def person = Person.find { it.id == 123 } (1)
def name = person?.name (2)
assert name == null (3)
1 | find 将返回一个 null 实例 |
2 | 使用null-safe运算符可防止出现 NullPointerException |
3 | 结果为 null |
2.6.2. 直接字段访问运算符
通常在Groovy中,当你编写这样的代码时:
class User {
public final String name (1)
User(String name) { this.name = name}
String getName() { "Name: $name" } (2)
}
def user = new User('Bob')
assert user.name == 'Name: Bob' (3)
1 | 公共字段 name |
2 | 返回自定义 name 字符串的getter |
3 | 调用getter |
user.name
调用触发对同名属性的调用,也就是说,在此处调用 name
的getter。如果要检索字段而不是调用getter,可以使用直接字段访问运算符:
assert user.@name == 'Bob' (1)
1 | 使用 .@ 访问字段而不是getter |
2.6.3. 方法指针运算符
方法指针运算符(.&
)调用用于存储对变量中方法的引用,以便稍后调用它:
def str = 'example of method reference' (1)
def fun = str.&toUpperCase (2)
def upper = fun() (3)
assert upper == str.toUpperCase() (4)
1 | str 变量包含一个 String |
2 | 我们在名为 fun 的变量上存储 str 实例上的 toUpperCase 方法的引用 |
3 | fun 可以像常规方法一样调用 |
4 | 我们可以检查结果是否与我们直接在 str 上调用它的结果相同 |
使用方法指针有许多优点。首先,这种方法指针的类型是 groovy.lang.Closure
,因此它可以在任何地方使用闭包。特别是,它适合转换现有方法以满足策略模式的需要:
def transform(List elements, Closure action) { (1)
def result = []
elements.each {
result << action(it)
}
result
}
String describe(Person p) { (2)
"$p.name is $p.age"
}
def action = this.&describe (3)
def list = [
new Person(name: 'Bob', age: 42),
new Person(name: 'Julia', age: 35)] (4)
assert transform(list, action) == ['Bob is 42', 'Julia is 35'] (5)
1 | transform 方法获取列表的每个元素并调用它们的 action 闭包,返回一个新列表 |
2 | 我们定义一个接收 Person 并返回 String 的函数 |
3 | 我们在该函数上创建一个方法指针 |
4 | 我们创建了我们想要收集描述符的元素列表 |
5 | 方法指针可以在包含 Closure 参数类型的函数中使用 |
方法指针由接收器和方法名称绑定。参数在运行时解析,这意味着如果你有多个具有相同名称的方法,则语法没有区别,只有在运行时才会调用相应方法的解析:
def doSomething(String str) { str.toUpperCase() } (1)
def doSomething(Integer x) { 2*x } (2)
def reference = this.&doSomething (3)
assert reference('foo') == 'FOO' (4)
assert reference(123) == 246 (5)
1 | 定义一个重载的 doSomething 方法,接受 String 作为参数 |
2 | 定义一个重载的 doSomething 方法,接受 Integer 作为参数 |
3 | 在 doSomething 上创建单个方法指针,而不指定参数类型 |
4 | 使用带有 String 的方法指针调用 doSomething 的 String 版本 |
5 | 使用带有 Integer 的方法指针调用 doSomething 的 Integer 版本 |
2.7. 正则表达式运算符
2.7.1. 模式运算符
模式运算符(~
)提供了一种创建 java.util.regex.Pattern
实例的简单方法:
def p = ~/foo/
assert p instanceof Pattern
通常,你会发现模式运算符在一个slashy字符串中有一个表达式,模式运算符可以与Groovy中的任何类型的 String
一起使用:
p = ~'foo' (1)
p = ~"foo" (2)
p = ~$/dollar/slashy $ string/$ (3)
p = ~"${pattern}" (4)
1 | 使用单引号字符串 |
2 | 使用双引号字符串 |
3 | 美元slashy字符串允许你使用斜杠和美元符号而不必转义它们 |
4 | 你也可以使用GString! |
2.7.2. 查找运算符
或者构建模式,你可以直接使用查找运算符 =~
来构建 java.util.regex.Matcher
实例:
def text = "some text to match"
def m = text =~ /match/ (1)
assert m instanceof Matcher (2)
if (!m) { (3)
throw new RuntimeException("Oops, text not found!")
}
1 | =~ 使用右侧的模式为 text 变量创建匹配器 |
2 | =~ 返回类型是 Matcher |
3 | 相当于调用 if (!m.find()) |
由于 Matcher
通过调用其 find
方法强制转换为 boolean
,因此 =~
运算符与Perl’s =~
运算符的简单用法一致,当它作为谓词出现时(if
,while
等)。
2.7.3. 匹配运算符
匹配运算符(==~
)是查找运算符的略微变化,它不返回 Matcher
而是返回布尔值,并且需要输入字符串的严格匹配:
m = text ==~ /match/ (1)
assert m instanceof Boolean (2)
if (m) { (3)
throw new RuntimeException("Should not reach that point!")
}
1 | ==~ 将主题与正则表达式严格匹配 |
2 | ==~ 返回类型因此是 boolean |
3 | 相当于调用 if (text ==~ /match/) |
2.8. 其他运算符
2.8.1. 传播运算符
Spread-dot Operator(*.
)通常缩写为Spread Operator,用于在聚合对象的所有条目上执行同一操作。它相当于对每个条目调用操作并将结果收集到列表中:
class Car {
String make
String model
}
def cars = [
new Car(make: 'Peugeot', model: '508'),
new Car(make: 'Renault', model: 'Clio')] (1)
def makes = cars*.make (2)
assert makes == ['Peugeot', 'Renault'] (3)
1 | 建立 Car 条目列表。该列表是对象的集合。 |
2 | 在列表中调用spread运算符,访问每个条目的 make 属性 |
3 | 返回与 make 项集合对应的字符串列表 |
表达式 cars*.make
相当于 cars.collect{ it.make }
。当引用的属性不是包含列表的属性时,Groovy的GPath表示法允许短路,在这种情况下,它会自动传播。在前面提到的情况下,也可以使用表达式 cars.make
,但通常建议保留显式spread-dot运算符。
传播运算符是null安全的,这意味着如果集合的元素为null,它将返回null而不是抛出 NullPointerException
:
cars = [
new Car(make: 'Peugeot', model: '508'),
null, (1)
new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault'] (2)
assert null*.make == null (3)
1 | 构建一个列表,其中的一个元素为 null |
2 | 使用传播运算符不会抛出 NullPointerException |
3 | 接收方也可能为null,在这种情况下返回值为 null |
传播运算符可用于任何实现 Iterable
接口的类:
class Component {
Long id
String name
}
class CompositeObject implements Iterable<Component> {
def components = [
new Component(id: 1, name: 'Foo'),
new Component(id: 2, name: 'Bar')]
@Override
Iterator<Component> iterator() {
components.iterator()
}
}
def composite = new CompositeObject()
assert composite*.id == [1,2]
assert composite*.name == ['Foo','Bar']
在处理本身包含聚合数据结构的聚合时,可使用spread-dot运算符的多次调用(此处为 cars*.models*.name
):
class Make {
String name
List<Model> models
}
@Canonical
class Model {
String name
}
def cars = [
new Make(name: 'Peugeot',
models: [new Model('408'), new Model('508')]),
new Make(name: 'Renault',
models: [new Model('Clio'), new Model('Captur')])
]
def makes = cars*.name
assert makes == ['Peugeot', 'Renault']
def models = cars*.models*.name
assert models == [['408', '508'], ['Clio', 'Captur']]
assert models.sum() == ['408', '508', 'Clio', 'Captur'] // flatten one level
assert models.flatten() == ['408', '508', 'Clio', 'Captur'] // flatten all levels (one in this case)
考虑对集合的集合使用 collectNested
DGM方法而不是spread-dot运算符:
class Car {
String make
String model
}
def cars = [
[
new Car(make: 'Peugeot', model: '408'),
new Car(make: 'Peugeot', model: '508')
], [
new Car(make: 'Renault', model: 'Clio'),
new Car(make: 'Renault', model: 'Captur')
]
]
def models = cars.collectNested{ it.model }
assert models == [['408', '508'], ['Clio', 'Captur']]
传播方法参数
在某些情况下,可以在需要适应方法参数的列表中找到方法调用的参数。在这种情况下,你可以使用传播运算符来调用该方法。例如,假设你有以下方法签名:
int function(int x, int y, int z) {
x*y+z
}
那么如果你有以下列表:
def args = [4,5,6]
你可以在不必定义中间变量的情况下调用该方法:
assert function(*args) == 26
甚至可以将普通参数与传播参数混合:
args = [4]
assert function(*args,5,6) == 26
传播列表元素
在列表中使用时,传播运算符就像将展开元素内容内联到列表中一样:
def items = [4,5] (1)
def list = [1,2,3,*items,6] (2)
assert list == [1,2,3,4,5,6] (3)
1 | items 是一个列表 |
2 | 我们想直接将 items 列表的内容插入到 list 中,而无需调用 addAll |
3 | items 的内容已内联到 list 中 |
传播映射元素
传播映射运算符的工作方式与传播列表运算符类似,但对于映射,它允许你将映射的内容内联到另一个映射中,如下例所示:
def m1 = [c:3, d:4] (1)
def map = [a:1, b:2, *:m1] (2)
assert map == [a:1, b:2, c:3, d:4] (3)
1 | m1 是我们想要内联的映射 |
2 | 我们使用 *:m1 表示法将 m1 的内容传播到 map 中 |
3 | map 包含 m1 的所有元素 |
传播映射运算符是位置相关的,如以下示例所示:
def m1 = [c:3, d:4] (1)
def map = [a:1, b:2, *:m1, d: 8] (2)
assert map == [a:1, b:2, c:3, d:8] (3)
1 | m1 是我们想要内联的映射 |
2 | 我们使用 *:m1 表示法将 m1 的内容传播到 map 中,但传播后重新定义键 d |
3 | map 包含所有预期的键,但 d 被重新定义 |
2.8.2. 范围运算符
Groovy支持范围的概念,并提供一个符号(..
)来创建对象范围:
def range = 0..5 (1)
assert (0..5).collect() == [0, 1, 2, 3, 4, 5] (2)
assert (0..<5).collect() == [0, 1, 2, 3, 4] (3)
assert (0..5) instanceof List (4)
assert (0..5).size() == 6 (5)
1 | 一个简单的整数范围,存储在局部变量中 |
2 | 一个 IntRange ,包含边界 |
3 | 一个 IntRange ,排除上限值 |
4 | groovy.lang.Range 实现了 List 接口 |
5 | 你可以在上面调用 size 方法 |
范围实现是轻量级的,这意味着只存储下限和上限。你可以从具有 next()
和 previous()
方法的任何 Comparable
对象创建范围,以确定范围中的下一个/上一个条目。例如,你可以通过以下方式创建一系列字符:
assert ('a'..'d').collect() == ['a','b','c','d']
2.8.3. 飞船运算符
宇宙飞船运算符(<=>
)委托调用 compareTo
方法:
assert (1 <=> 1) == 0
assert (1 <=> 2) == -1
assert (2 <=> 1) == 1
assert ('a' <=> 'z') == -1
2.8.4. 下标运算符
下标运算符是 getAt
或 putAt
的简写符号,具体取决于你是在赋值的左侧还是右侧使用它:
def list = [0,1,2,3,4]
assert list[2] == 2 (1)
list[2] = 4 (2)
assert list[0..2] == [0,1,4] (3)
list[0..2] = [6,6,6] (4)
assert list == [6,6,6,3,4] (5)
1 | 可以用 [2] 代替 getAt(2) |
2 | 如果在赋值符号的左侧,将调用 putAt |
3 | getAt 还支持范围 |
4 | putAt 也是如此 |
5 | 变异后的列表 |
下标运算符与 getAt
/putAt
的自定义实现相结合是解构对象的便捷方法:
class User {
Long id
String name
def getAt(int i) { (1)
switch (i) {
case 0: return id
case 1: return name
}
throw new IllegalArgumentException("No such element $i")
}
void putAt(int i, def value) { (2)
switch (i) {
case 0: id = value; return
case 1: name = value; return
}
throw new IllegalArgumentException("No such element $i")
}
}
def user = new User(id: 1, name: 'Alex') (3)
assert user[0] == 1 (4)
assert user[1] == 'Alex' (5)
user[1] = 'Bob' (6)
assert user.name == 'Bob' (7)
1 | User 类定义了一个自定义 getAt 实现 |
2 | User 类定义了一个自定义 putAt 实现 |
3 | 创建一个示例用户 |
4 | 使用索引为0的下标运算符检索用户id |
5 | 使用索引为1的下标运算符检索用户name |
6 | 我们可以使用下标运算符写入属性值,这要归功于 putAt 的委托 |
7 | 并检查它是否真的已更改 name 属性 |
2.8.5. 成员运算符
成员运算符(in
)等同于调用 isCase
方法。在 List
的上下文中,它等同于调用 contains
,如下例所示:
def list = ['Grace','Rob','Emmy']
assert ('Emmy' in list) (1)
1 | 相当于调用 list.contains('Emmy') 或 list.isCase('Emmy') |
2.8.6. 身份运算符
在Groovy中,使用 ==
来测试相等性与在Java中使用相同的运算符不同。在Groovy中,它调用 equals
。如果要比较引用相等性,则应使用如下例所示:
def list1 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] (1)
def list2 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] (2)
assert list1 == list2 (3)
assert !list1.is(list2) (4)
1 | 创建字符串列表 |
2 | 创建另一个包含相同元素的字符串列表 |
3 | 使用 == ,我们测试对象相等性 |
4 | 但是使用 is ,我们可以检查引用是不同的 |
2.8.7. 强转运算符
强转运算符(as
)是casting的变体。强制将对象从一种类型转换为另一种类型而不兼容赋值。我们来举个例子:
Integer x = 123
String s = (String) x (1)
1 | Integer 不能赋值给 String ,因此它会在运行时产生 ClassCastException |
这可以通过使用强转运算符来修复:
Integer x = 123
String s = x as String (1)
1 | Integer 不能赋值给 String ,但使用 as 会将其强制转换为 String |
当一个对象被强制转换为另一个对象时,除非目标类型与源类型相同,否则强转将返回一个新对象。转换规则因源和目标类型而异,如果没有找到转换规则,强转可能会失败。可用 asType
方法实现自定义转换规则:
class Identifiable {
String name
}
class User {
Long id
String name
def asType(Class target) { (1)
if (target == Identifiable) {
return new Identifiable(name: name)
}
throw new ClassCastException("User cannot be coerced into $target")
}
}
def u = new User(name: 'Xavier') (2)
def p = u as Identifiable (3)
assert p instanceof Identifiable (4)
assert !(p instanceof User) (5)
1 | User 类定义从 User 到 Identifiable 的自定义转换规则 |
2 | 我们创建一个 User 实例 |
3 | 我们将 User 实例强制转换为 Identifiable |
4 | 目标实例是 Identifiable 类型 |
5 | 目标不再是 User 类型的实例 |
2.8.8. 钻石运算符
钻石运算符(<>
)是一个仅用于语法的运算符,用于支持与Java 7中相同名称的运算符的兼容性。它用于指示应从声明中推断泛型类型:
List<String> strings = new LinkedList<>()
在动态Groovy中,这是完全无用的。在静态类型检查的Groovy中,它也是可选的,因为Groovy类型检查器会执行类型推断,不管这个操作符是否存在。
2.8.9. 调用运算符
调用运算符(()
)用于隐式调用名为 call
的方法。对于定义调用方法的任何对象,可以省略 .call
部分并使用调用运算符:
class MyCallable {
int call(int x) { (1)
2*x
}
}
def mc = new MyCallable()
assert mc.call(2) == 4 (2)
assert mc(2) == 4 (3)
1 | MyCallable 定义了一个名为 call 的方法。请注意,它不需要实现 java.util.concurrent.Callable |
2 | 我们可以使用经典方法调用语法来调用该方法 |
3 | 或者我们可以通过调用运算符省略 .call |
2.9. 运算符优先级
下表按优先顺序列出了所有常规运算符。
级别 |
运算符 |
名称 |
1 |
|
对象创建,显式小括号 |
|
方法调用,闭包,列表/映射 |
|
|
成员访问,方法闭包,字段/属性访问 |
|
|
安全解引用,spread, spread-dot, spread-map |
|
|
按位否定/模式,逻辑非,类型转换 |
|
|
列表/数组/映射索引访问,后置自增/自减 |
|
2 |
|
求幂 |
3 |
|
前置自增/自减,正,负 |
4 |
|
乘法,除法,取余 |
5 |
|
加法,减法 |
6 |
|
左移,右移,无符号右移,范围,排除上限范围 |
7 |
|
小于,小于等于,大于,大于等于,in,instanceof,类型强转 |
8 |
|
等于,不等于,比较 |
|
查找正则表达式,匹配正则表达式 |
|
9 |
|
二进制按位与 |
10 |
|
二进制按位异或 |
11 |
|
二进制按位或 |
12 |
|
逻辑与 |
13 |
|
逻辑或 |
14 |
|
三元条件 |
|
elvis运算符 |
|
15 |
|
各种赋值 |
2.10. 运算符重载
Groovy允许你重载各种运算符,以便它们可以与你自己的类一起使用。考虑这个简单的类:
class Bucket {
int size
Bucket(int size) { this.size = size }
Bucket plus(Bucket other) { (1)
return new Bucket(this.size + other.size)
}
}
1 | Bucket 实现了一个名为 plus() 的特殊方法 |
只需实现 plus()
方法,Bucket
类现在可以与 +
运算符一起使用,如下所示:
def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15 (1)
1 | 两个 Bucket 对象可以与 + 运算符一起进行加运算 |
所有(非比较器)Groovy运算符都有一个相应的方法,你可以在自己的类中实现。唯一的要求是你的方法是公共的,具有正确的名称,并具有正确的参数数量。参数类型取决于你希望在运算符右侧支持的类型。例如,你可以支持该声明:
assert (b1 + 11).size == 15
通过使用此签名实现 plus()
方法:
Bucket plus(int capacity) {
return new Bucket(this.size + capacity)
}
以下是运算符及其对应方法的完整列表:
运算符 |
方法 |
运算符 |
方法 |
|
a.plus(b) |
|
a.getAt(b) |
|
a.minus(b) |
|
a.putAt(b, c) |
|
a.multiply(b) |
|
b.inCase(a) |
|
a.div(b) |
|
a.leftShift(b) |
|
a.mod(b) |
|
a.rightShift(b) |
|
a.power(b) |
|
a.rightShiftUnsigned(b) |
|
a.or(b) |
|
a.next() |
|
a.and(b) |
|
a.previous() |
|
a.xor(b) |
|
a.positive() |
|
a.asType(b) |
|
a.negative() |
|
a.call() |
|
a.bitwiseNegate() |
3. 程序结构
本章介绍Groovy编程语言的程序结构。
3.1. 包名称
包名称与Java中的角色完全相同。它们允许我们在没有任何冲突的情况下分离代码库。Groovy类必须在类定义之前指定它们的包,否则假定是默认包。
定义包与Java非常相似:
// 定义名为com.yoursite的包
package com.yoursite
要在 com.yoursite.com
包中引用某个类 Foo
,你需要使用完全限定名 com.yoursite.com.Foo
,否则你可以使用 import
语句,如下所示。
3.2. 导入
要引用任何类,你需要对其包的限定引用。Groovy遵循Java中允许 import
语句来解析类引用的概念。
例如,Groovy提供了几个构建器类,例如 MarkupBuilder
。MarkupBuilder
在包 groovy.xml
中,所以为了使用这个类,你需要 import
它,如下所示:
// 导入类MarkupBuilder
import groovy.xml.MarkupBuilder
// 使用导入的类创建对象
def xml = new MarkupBuilder()
assert xml != null
3.2.1. 默认导入
默认导入是Groovy语言默认提供的导入。例如,查看以下代码:
new Date()
Java中的相同代码需要一个 Date
类的import语句,如下所示:import java.util.Date。默认情况下,Groovy会为你导入这些类。
以下导入由groovy为你默认添加:
import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal
这样做是因为这些包中的类是最常用的。通过导入可以减少这些样板代码。
3.2.2. 简单的导入
一个简单的导入是一个import语句,你可以在其中完全定义类名和包。例如,下面代码中的import语句import groovy.xml.MarkupBuilder是一个简单的导入,它直接引用包内的类。
// 导入类MarkupBuilder
import groovy.xml.MarkupBuilder
// 使用导入的类创建对象
def xml = new MarkupBuilder()
assert xml != null
3.2.3. 星号导入
像Java一样,Groovy提供了一种特殊的方法,可以使用 *
来导入包中的所有类,即所谓的星号导入。MarkupBuilder是一个包在groovy.xml包中的类,以及另一个名为StreamingMarkupBuilder的类。如果你需要使用这两个类,你可以:
import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder
def markupBuilder = new MarkupBuilder()
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
这是完全有效的代码。但是使用 *
导入,我们只用一行就能达到同样的效果。星号导入包 groovy.xml
下的所有类:
import groovy.xml.*
def markupBuilder = new MarkupBuilder()
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
*
导入的一个问题是它们可能会混乱你的本地命名空间。但是由于Groovy提供了各种别名,这很容易解决。
3.2.4. 静态导入
Groovy的静态导入功能允许你引用导入的类,就像它们是你自己的类中的静态方法一样:
import static Boolean.FALSE
assert !FALSE // 直接使用,没有Boolean前缀!
这类似于Java的静态导入功能,但它比Java更具动态性,因为它允许你定义与导入方法同名的方法,只要你有不同的类型:
import static java.lang.String.format (1)
class SomeClass {
String format(Integer i) { (2)
i.toString()
}
static void main(String[] args) {
assert format('String') == 'String' (3)
assert new SomeClass().format(Integer.valueOf(1)) == '1'
}
}
1 | 静态导入方法 |
2 | 与上面静态导入的方法同名的方法声明,但具有不同的参数类型 |
3 | 在java中编译错误,但在groovy中是有效的代码 |
如果你具有相同的类型,则导入的类优先。
3.2.5. 静态导入别名
使用 as
关键字的静态导入为命名空间问题提供了一个优雅的解。假设你想使用其 getInstance()
方法获取 Calendar
实例。这是一个静态方法,所以我们可以使用静态导入。但是不是每次都调用 getInstance()
,这可能会在与类名分离时产生误导,我们可以使用别名导入它,以提高代码可读性:
import static Calendar.getInstance as now
assert now().class == Calendar.getInstance().class
3.2.6. 静态星号导入
静态星号导入与常规星号导入非常相似。它将导入给定类的所有静态方法。
例如,假设我们需要为我们的应用程序计算正弦和余弦。java.lang.Math类有一个名为 sin
和 cos
的静态方法,它们符合我们的需要。借助静态星号导入,我们可以:
import static java.lang.Math.*
assert sin(0) == 0.0
assert cos(0) == 1.0
如你所见,我们能够直接访问 sin
和 cos
方法,而无需 Math.
前缀。
3.2.7. 导入别名
使用类型别名,我们可以使用我们选择的名称来引用完全限定的类名。这可以使用 as
关键字完成,如前所述。
例如,我们可以将 java.sql.Date
作为 SQLDate
导入,并在与 java.util.Date
相同的文件中使用它,而不必使用任一类的完全限定名称:
import java.util.Date
import java.sql.Date as SQLDate
Date utilDate = new Date(1000L)
SQLDate sqlDate = new SQLDate(1000L)
assert utilDate instanceof java.util.Date
assert sqlDate instanceof java.sql.Date
3.3. 脚本VS类
3.3.1. public static void main vs script
Groovy支持脚本和类。以下面的代码为例:
class Main { (1)
static void main(String... args) { (2)
println 'Groovy world!' (3)
}
}
1 | 定义一个 Main 类,名称是任意的 |
2 | public static void main(String[]) 方法可用作类的主方法 |
3 | 方法的主体 |
这是你可以从Java中找到的典型代码,其中代码必须嵌入到可执行的类中。
println 'Groovy world!'
脚本可以被视为一个类而不需要声明它,但有一些差异。
3.3.2. 脚本类
脚本 始终编译为类。Groovy编译器将为你编译该类,并将脚本的主体复制到 run
方法中。因此,前面的示例被编译为如下所示:
import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script { (1)
def run() { (2)
println 'Groovy world!' (3)
}
static void main(String[] args) { (4)
InvokerHelper.runScript(Main, args) (5)
}
}
1 | Main 类继承自 groovy.lang.Script 类 |
2 | groovy.lang.Script 需要一个返回值的 run 方法 |
3 | 脚本主体写在 run 方法内 |
4 | main 方法是自动生成的 |
5 | 并在 run 方法上委派脚本的执行 |
如果脚本位于文件中,则使用该文件的基本名称来确定生成的脚本类的名称。在此示例中,如果文件的名称为 Main.groovy
,则脚本类将为 Main
。
3.3.3. 方法
可以在脚本中定义方法,如下所示:
int fib(int n) {
n < 2 ? 1 : fib(n-1) + fib(n-2)
}
assert fib(10)==89
你还可以混合方法和代码。生成的脚本类将所有方法都包含在脚本类中,并将所有脚本体组装到 run
方法中:
println 'Hello' (1)
int power(int n) { 2**n } (2)
println "2^6==${power(6)}" (3)
1 | 脚本开始 |
2 | 在脚本体中定义了一个方法 |
3 | 和脚本继续 |
此代码在内部转换为:
import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script {
int power(int n) { 2** n} (1)
def run() {
println 'Hello' (2)
println "2^6==${power(6)}" (3)
}
static void main(String[] args) {
InvokerHelper.runScript(Main, args)
}
}
1 | 将 power 方法原样复制到生成的脚本类中 |
2 | 第一个语句被复制到 run 方法中 |
3 | 第二个语句被复制到 run 方法中 |
即使Groovy从你的脚本创建了一个类,它对用户来说也是完全透明的。特别是,脚本被编译为字节码,并保留行号。这意味着如果在脚本中抛出异常,堆栈跟踪将显示与原始脚本对应的行号,而不是我们显示的生成代码。
4. 面向对象
本章介绍Groovy编程语言的面向对象。
4.1. 类型
4.1.1. 原始类型
Groovy支持与Java语言规范定义的原始类型相同的原始类型:
-
整数类型:
byte
(8位),short
(16位),int
(32位)和long
(64位) -
浮点类型:
float
(32位)和double
(64位) -
boolean
类型(true
或false
) -
char
类型(16位,可用作数字类型,表示UTF-16代码)
虽然Groovy声明并将原始字段和变量存储为基元,但因为它将对象用于所有内容,所以它会自动对基元进行引用。就像Java一样,它使用的包装器也是如此
原始类型 |
包装类 |
boolean |
Boolean |
char |
Character |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
这是一个使用 int
的例子
class Foo {
static int i
}
assert Foo.class.getDeclaredField('i').type == int.class
assert Foo.i.class != int.class && Foo.i.class == Integer.class
现在你可能会担心这意味着每次在对基元的引用上使用数学运算符时,你将承担拆箱和装箱基元的成本。但事实并非如此,因为Groovy会将你的运算符编译为其方法等价物并使用它们。此外,Groovy在调用接受原语参数的Java方法时,会自动拆箱到基元类型,并自动装箱从Java方法返回的值。但请注意,Java的方法解析存在一些差异。
4.1.2. 类
Groovy类与Java类非常相似,并且与JVM级别的Java兼容。它们可能有方法,字段和属性(想想JavaBean属性但是比其样板代码少)。类和类成员可以使用与Java中相同的修饰符(public,protected,private,static等),在源代码级别有一些细微差别,稍后会对其进行解释。
Groovy类与Java对应类之间的主要区别是:
-
没有可见性修饰符的类或方法会自动公开(可以使用特殊注解来实现包私有可见性)。
-
没有可见性修饰符的字段会自动转换为属性,这样可以减少冗长的代码,因为不需要显式的getter和setter方法。有关此方面的更多信息将在字段和属性部分中介绍。
-
类不需要与源文件定义具有相同的基本名称,但在大多数情况下强烈建议使用它们(另请参阅有关脚本的下一点)。
-
一个源文件可能包含一个或多个类(但如果文件包含不在类中的任何代码,则将其视为脚本)。脚本只是具有一些特殊约定的类,并且与源文件具有相同的名称(因此不要在脚本中包含与脚本源文件同名的类定义)。
以下代码提供了一个示例类:
class Person { (1)
String name (2)
Integer age
def increaseAge(Integer years) { (3)
this.age += years
}
}
1 | 类开头,名为 Person |
2 | 字符串字段和名为 name 的属性 |
3 | 方法定义 |
普通类
普通类指的是顶级和具体的类。这意味着它们可以在没有任何其他类或脚本限制的情况下实例化。这样,它们只能是公共的(即使可以抑制public关键字)。通过使用new关键字调用其构造函数来实例化类,如下面的代码段所示:
def p = new Person()
内部类
内部类在另一个类中定义。封闭类可以像往常一样使用内部类。另一方面,内部类可以访问其封闭类的成员,即使它们是私有的。封闭类以外的类不允许访问内部类。这是一个例子:
class Outer {
private String privateStr
def callInnerMethod() {
new Inner().methodA() (1)
}
class Inner { (2)
def methodA() {
println "${privateStr}." (3)
}
}
}
1 | 内部类被实例化并调用其方法 |
2 | 内部类定义,在其封闭类中 |
3 | 即使是私有的,内部类也可以访问封闭类的私有字段 |
使用内部类有一些原因:
-
它们通过将内部类隐藏在其他类中来增加封装,当这些类不需要知道它时。这也导致更清洁的包装和工作空间。
-
它们通过对仅由一个类使用的类进行分组来提供良好的组织。
-
它们导致更易维护的代码,因为内部类紧邻使用它们的类。
在某些情况下,内部类是接口的实现,其外部类需要调用其方法。下面的代码通过使用线程来说明这一点,这很常见。
class Outer2 {
private String privateStr = 'some string'
def startThread() {
new Thread(new Inner2()).start()
}
class Inner2 implements Runnable {
void run() {
println "${privateStr}."
}
}
}
请注意,类 Inner2
的定义仅用于提供 run
方法到类 Outer2
的实现。在这种情况下,匿名内部类有助于消除冗长。
匿名内部类
内部类的最后一个示例可以使用匿名内部类进行简化。使用以下代码可以实现相同的功能。
class Outer3 {
private String privateStr = 'some string'
def startThread() {
new Thread(new Runnable() { (1)
void run() {
println "${privateStr}."
}
}).start() (2)
}
}
1 | 与上一节的最后一个例子相比,new Inner2() 被 new Runnable() 及其所有实现所取代 |
2 | 正常调用 start 方法 |
因此,不需要定义仅使用一次的新类。
抽象类
抽象类表示通用概念,因此,它们无法实例化,被创建为子类。他们的成员包括字段/属性和抽象或具体方法。抽象方法没有实现,必须由具体的子类实现。
abstract class Abstract { (1)
String name
abstract def abstractMethod() (2)
def concreteMethod() {
println 'concrete'
}
}
1 | 必须使用 abstract 关键字声明抽象类 |
2 | 抽象方法也必须用 abstract 关键字声明 |
通常将抽象类与接口进行比较。但是选择一个或另一个至少有两个重要的区别。首先,虽然抽象类可能包含字段/属性和具体方法,但接口可能只包含抽象方法(方法签名)。而且,一个类可以实现几个接口,而它只可以继承一个抽象或不抽象类。
4.1.3. 接口
接口定义了类需要遵循的契约。接口仅定义需要实现的方法列表,但不定义方法实现。
interface Greeter { (1)
void greet(String name) (2)
}
1 | 需要使用 interface 关键字声明接口 |
2 | 接口只定义方法签名 |
接口的方法总是公开的。在接口中使用 protected
或 private
方法是错误的:
interface Greeter {
protected void greet(String name) (1)
}
1 | 使用 protected 会报编译时错误 |
如果类在其 implements
列表中定义了接口或者其任何超类实现了接口,则该类实现了某接口:
class SystemGreeter implements Greeter { (1)
void greet(String name) { (2)
println "Hello $name"
}
}
def greeter = new SystemGreeter()
assert greeter instanceof Greeter (3)
1 | SystemGreeter 使用 implements 关键字声明 Greeter 接口 |
2 | 然后实现所需的 greet 方法 |
3 | SystemGreeter 的任何实例也是 Greeter 接口的实例 |
接口可以继承另一个接口:
interface ExtendedGreeter extends Greeter { (1)
void sayBye(String name)
}
1 | ExtendedGreeter 接口使用 extends 关键字继承 Greeter 接口 |
值得注意的是,对于一个类是一个接口的实例,它必须是显式声明的。例如,以下类定义 greet
方法,因为它在 Greeter
接口中声明,但未在其接口中声明实现 Greeter
:
class DefaultGreeter {
void greet(String name) { println "Hello" }
}
greeter = new DefaultGreeter()
assert !(greeter instanceof Greeter)
换句话说,Groovy没有定义结构类型。但是,可以使用 as
强转运算符使对象的实例在运行时实现接口:
greeter = new DefaultGreeter() (1)
coerced = greeter as Greeter (2)
assert coerced instanceof Greeter (3)
1 | 创建一个不实现该接口的 DefaultGreeter 实例 |
2 | 在运行时将实例强制转换为 Greeter |
3 | 强转后的实例实现了 Greeter 接口 |
你可以看到有两个不同的对象:一个是源对象,一个 DefaultGreeter
实例,它不实现接口。另一个是 Greeter
的一个实例,它委托给强转对象。
Groovy接口不支持Java 8接口等默认实现。如果你正在寻找类似(但不相等)的东西,则特征接近接口,但允许默认实现以及本手册中描述的其他重要功能。
4.1.4. 构造函数
构造函数是用于初始化具有特定状态的对象的特殊方法。与普通方法一样,只要每个构造函数具有唯一的类型签名,类就可以声明多个构造函数。如果对象在构造期间不需要任何参数,则可以使用无参构造函数。如果没有提供构造函数,Groovy编译器将提供一个空的无参数构造函数。
Groovy支持两种调用样式:
-
位置参数的使用方式与使用Java构造函数的方式类似
-
命名参数允许你在调用构造函数时指定参数名称
位置参数
要使用位置参数创建对象,相应的类需要声明一个或多个构造函数。在多个构造函数的情况下,每个构造函数必须具有唯一的类型签名。构造函数也可以使用 groovy.transform.TupleConstructor
注解添加到类中。
通常,一旦声明了至少一个构造函数,该类只能通过调用其构造函数来实例化。值得注意的是,在这种情况下,你通常无法使用命名参数创建类。Groovy支持命名参数,只要该类包含一个无参数构造函数或提供一个构造函数,该构造函数将 Map
参数作为第一个(也可能是唯一的)参数 - 有关详细信息,请参阅下一节。
使用声明的构造函数有三种形式。第一个是普通的Java方式,使用 new
关键字。其他的方式依赖于将列表强制转换为所需类型。在这种情况下,可以强制使用 as
关键字并通过静态键入变量。
class PersonConstructor {
String name
Integer age
PersonConstructor(name, age) { (1)
this.name = name
this.age = age
}
}
def person1 = new PersonConstructor('Marie', 1) (2)
def person2 = ['Marie', 2] as PersonConstructor (3)
PersonConstructor person3 = ['Marie', 3] (4)
1 | 构造函数声明 |
2 | 构造函数调用,经典的Java方式 |
3 | 使用 as 关键字强转的构造函数用法 |
4 | 构造函数的使用,在赋值时使用强转 |
命名参数
如果没有声明(或无参数)构造函数,则可以通过以映射(属性/值对)的形式传递参数来创建对象。在需要允许多个参数组合的情况下,这可以派上用场。否则,通过使用传统的位置参数,有必要声明所有可能的构造函数。有一个构造函数,其中第一个(也许只是唯一的)参数是一个 Map
参数也是支持的 - 这样的构造函数也可以使用 groovy.transform.MapConstructor
注解标注。
class PersonWOConstructor { (1)
String name
Integer age
}
def person4 = new PersonWOConstructor() (2)
def person5 = new PersonWOConstructor(name: 'Marie') (3)
def person6 = new PersonWOConstructor(age: 1) (4)
def person7 = new PersonWOConstructor(name: 'Marie', age: 2) (5)
1 | 没有声明构造函数 |
2 | 实例化中没有给出参数 |
3 | 实例化中给出的 name 参数 |
4 | 实例化中给出的 age 参数 |
5 | 实例化中给出的 name 和 age 参数 |
然而,重要的是要强调,这种方法为构造函数调用者提供了更多的功能,同时增加了调用者的责任,以使名称和值类型正确。因此,如果需要更大的控制,则可能优选使用位置参数来声明构造函数。
|
4.1.5. 方法
Groovy方法与其他语言非常相似。一些特点将在下一小节中展示。
方法定义
使用返回类型或使用 def
关键字定义方法,以使返回类型无类型化。方法还可以接收任意数量的参数,这些参数可能没有显式声明其类型。Java修饰符可以正常使用,如果没有提供可见性修饰符,则该方法是公共的。
Groovy中的方法总是返回一些值。如果未提供 return
语句,则将返回在执行的最后一行中计算的值。例如,请注意以下方法都不使用 return
关键字。
def someMethod() { 'method called' } (1)
String anotherMethod() { 'another method called' } (2)
def thirdMethod(param1) { "$param1 passed" } (3)
static String fourthMethod(String param1) { "$param1 passed" } (4)
1 | 声明没有返回类型且没有参数的方法 |
2 | 具有显式返回类型且无参数的方法 |
3 | 没有定义参数类型的方法 |
4 | 带 String 参数的静态方法 |
命名参数
与构造函数一样,也可以使用命名参数调用常规方法。为了支持这种表示法,使用了一种约定,其中方法的第一个参数是 Map
。在方法体中,可以像在普通Map中一样访问参数值(map.key
)。如果该方法只有一个 Map
参数,则必须命名所有提供的参数。
def foo(Map args) { "${args.name}: ${args.age}" }
foo(name: 'Marie', age: 1)
混合命名和位置参数
命名参数可以与位置参数混合。在这种情况下,除了将 Map
参数作为第一个参数之外,相同的约定也适用,所讨论的方法将根据需要具有其他位置参数。调用方法时提供的位置参数必须按顺序排列。命名参数可以在任何位置。它们被分组到Map中并自动作为第一个参数提供。
def foo(Map args, Integer number) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(name: 'Marie', age: 1, 23) (1)
foo(23, name: 'Marie', age: 1) (2)
1 | 使用 Integer 类型的附加 number 参数调用方法 |
2 | 改变了参数的顺序调用方法 |
如果我们没有 Map
作为第一个参数,那么必须为该参数提供Map而不是命名参数。如果不这样做将导致 groovy.lang.MissingMethodException
:
def foo(Integer number, Map args) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(name: 'Marie', age: 1, 23) (1)
1 | 方法调用抛出 groovy.lang.MissingMethodException: No signature of method: foo() is applicable for argument types: (LinkedHashMap, Integer) values: [[name:Marie, age:1], 23] ,因为命名参数 Map 参数未定义为第一个参数 |
如果我们使用显式 Map
参数替换命名参数,则可以避免上述异常:
def foo(Integer number, Map args) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(23, [name: 'Marie', age: 1]) (1)
1 | 显式 Map 参数代替命名参数使调用有效 |
虽然Groovy允许你混合命名和位置参数,但它可能会导致不必要的混淆。谨慎混合命名和位置参数。
默认参数
默认参数使参数可选。如果未提供参数,则该方法采用默认值。
def foo(String par1, Integer par2 = 1) { [name: par1, age: par2] }
assert foo('Marie').age == 1
可变参数
Groovy支持具有可变数量参数的方法。它们的定义如下:def foo(p1, …, pn, T… args)
。这里 foo
默认支持 n
个参数,但是还有一个未指定数量(超过 n
)的其他参数。
def foo(Object... args) { args.length }
assert foo() == 0
assert foo(1) == 1
assert foo(1, 2) == 2
这个例子定义了一个方法 foo
,它可以接受任意数量的参数,包括根本没有参数。args.length
将返回给定的参数数量。Groovy允许 T[]
作为 T…
的替代符号。这意味着任何带有数组作为最后一个参数的方法都被Groovy看作是一个可以获取可变数量参数的方法。
def foo(Object[] args) { args.length }
assert foo() == 0
assert foo(1) == 1
assert foo(1, 2) == 2
如果使用 null
调用可变参数方法作为其参数,则参数将为 null
,而不是长度为1的数组,其中 null
为唯一元素。
def foo(Object... args) { args }
assert foo(null) == null
如果使用数组作为参数调用可变参数方法,则参数将是该数组而不是包含给定数组作为唯一元素的长度为1的数组。
def foo(Object... args) { args }
Integer[] ints = [1, 2]
assert foo(ints) == [1, 2]
另一个重点是可变参数与方法重载相结合。在方法重载的情况下,Groovy将选择最具体的方法。例如,如果方法 foo
采用类型为 T
的可变参数,而另一个方法 foo
也采用类型为 T
的一个参数,则第二种方法是首选方法。
def foo(Object... args) { 1 }
def foo(Object x) { 2 }
assert foo() == 1
assert foo(1) == 2
assert foo(1, 2) == 1
方法选择算法
(TBD)
异常声明
Groovy自动允许你将检查异常视为非检查异常。这意味着你不需要声明任何方法可能抛出的检查异常,如以下示例所示,如果找不到该文件,则会抛出 FileNotFoundException
:
def badRead() {
new File('doesNotExist.txt').text
}
shouldFail(FileNotFoundException) {
badRead()
}
你也不需要在 try/catch
块中将上一个示例中的 badRead
方法的调用包围起来 - 尽管如果你愿意,你可以自由地执行此操作。
如果你希望声明代码可能抛出的任何异常(选中或以其他方式),你可以自由地执行此操作。添加异常不会改变从任何其他Groovy代码中使用代码的方式,但可以被视为代码读者的文档。异常将成为字节码中方法声明的一部分,因此如果你的代码可能是从Java调用的,那么包含它们可能会很有用。以下示例说明了使用显式检查异常声明:
def badRead() throws FileNotFoundException {
new File('doesNotExist.txt').text
}
shouldFail(FileNotFoundException) {
badRead()
}
4.1.6. 字段和属性
字段
字段是类或特征的成员,具有:
-
强制访问修饰符(
public
,protected
或private
) -
一个或多个可选修饰符(
static
,final
,synchronized
) -
可选类型
-
强制名称
class Data {
private int id (1)
protected String description (2)
public static final boolean DEBUG = false (3)
}
1 | 一个名为 id 的 private 字段,类型为 int |
2 | 一个名为 description 的 protected 字段,类型为 String |
3 | 一个名为 DEBUG 的 public static final 字段,类型为 boolean |
可以在声明时直接初始化字段:
class Data {
private String id = IDGenerator.next() (1)
// ...
}
1 | 使用 IDGenerator.next() 初始化私有字段 id |
可以省略字段的类型声明。然而,这被认为是一种不好的做法,一般来说,对字段使用强类型是个好主意:
class BadPractice {
private mapping (1)
}
class GoodPractice {
private Map<String,String> mapping (2)
}
1 | 字段 mapping 不声明类型 |
2 | 字段 mapping 具有强类型 |
如果你想稍后使用可选类型检查,则两者之间的区别很重要。它对文档也很重要。但是在某些情况下,如脚本或者如果你想依赖鸭子类型,省略类型可能会很有趣。
属性
属性是类的外部可见特征。不仅仅使用公共字段来表示这些特性(提供更有限的抽象并限制重构的可能性),Java中的典型约定是遵循JavaBean约定,即使用私有字段和getter/setter方法的组合来表示属性。Groovy遵循这些相同的约定,但提供了一种更简单的方法来定义属性。你可以使用以下内容定义属性:
-
缺省访问修饰符(不是
public
,protected
或private
) -
一个或多个可选修饰符(
static
,final
,synchronized
) -
可选类型
-
强制名称
然后Groovy将适当地生成getter/setter。例如:
class Person {
String name (1)
int age (2)
}
1 | 创建一个支持 private String name 字段,一个 getName 和一个 setName 方法 |
2 | 创建一个支持 private int age 字段,一个 getAge 和一个 setAge 方法 |
如果属性被声明为 final
,则不会生成setter方法:
class Person {
final String name (1)
final int age (2)
Person(String name, int age) {
this.name = name (3)
this.age = age (4)
}
}
1 | 定义 String 类型的只读属性 |
2 | 定义 int 类型的只读属性 |
3 | 将 name 参数指定给 name 字段 |
4 | 将 age 参数指定给 age 字段 |
属性按名称访问,并将透明地调用getter或setter,除非代码位于定义属性的类中:
class Person {
String name
void name(String name) {
this.name = "Wonder$name" (1)
}
String wonder() {
this.name (2)
}
}
def p = new Person()
p.name = 'Marge' (3)
assert p.name == 'Marge' (4)
p.name('Marge') (5)
assert p.wonder() == 'WonderMarge' (6)
1 | this.name 将直接访问该字段,因为该属性是从定义它的类中访问的 |
2 | 类似地,直接在 name 字段上进行读取访问 |
3 | 对属性的写访问是在 Person 类之外完成的,因此它将隐式调用 setName |
4 | 对属性的读访问是在 Person 类之外完成的,因此它将隐式调用 getName |
5 | 这将调用 Person 上的 name 方法,该方法执行对该字段的直接访问 |
6 | 这将调用 Person 上的 wonder 方法,该方法对该字段执行直接读访问 |
值得注意的是,直接访问支持字段的这种行为是为了防止在定义属性的类中使用属性访问语法时堆栈溢出。
由于实例的元 properties
字段,可以列出类的所有属性:
class Person {
String name
int age
}
def p = new Person()
assert p.properties.keySet().containsAll(['name','age'])
按照惯例,即使没有支持字段,Groovy也会识别属性,前提是存在遵循Java Bean规范的getter或setter方法。例如:
class PseudoProperties {
// a pseudo property "name"
void setName(String name) {}
String getName() {}
// a pseudo read-only property "age"
int getAge() { 42 }
// a pseudo write-only property "groovy"
void setGroovy(boolean groovy) { }
}
def p = new PseudoProperties()
p.name = 'Foo' (1)
assert p.age == 42 (2)
p.groovy = true (3)
1 | 允许写 p.name ,因为有一个伪属性 name |
2 | 允许读取 p.age ,因为存在伪只读属性 age |
3 | 允许写 p.groovy ,因为有一个伪只写属性 groovy |
这种语法糖是Groovy编写的众多DSLs的核心。
4.1.7. 注解
注解定义
注解是一种专用于注释代码元素的特殊接口。注解是一种超接口是 Annotation
接口的类型。使用 @interface
关键字以与接口非常相似的方式声明注解:
@interface SomeAnnotation {}
注解可以以没有实体和可选默认值的方法形式定义成员。可能的成员类型仅限于:
例如:
@interface SomeAnnotation {
String value() (1)
}
@interface SomeAnnotation {
String value() default 'something' (2)
}
@interface SomeAnnotation {
int step() (3)
}
@interface SomeAnnotation {
Class appliesTo() (4)
}
@interface SomeAnnotation {}
@interface SomeAnnotations {
SomeAnnotation[] value() (5)
}
enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
@interface Scheduled {
DayOfWeek dayOfWeek() (6)
}
1 | 定义 String 类型的值成员的注解 |
2 | 定义 String 类型的值成员的注解,其默认值为 something |
3 | 定义类型为基本类型 int 的 step 成员的注解 |
4 | 定义 Class 类型的 applyTo 成员的注解 |
5 | 定义 value 成员的注解,该类型是另一个注解类型的数组 |
6 | 定义 dayOfWeek 成员的注解,其类型是枚举类型 DayOfWeek |
与Java语言不同,在Groovy中,注解可用于更改语言的语义。AST转换尤其如此,它将基于注解生成代码。
注释放置
注解可以应用于代码的各种元素上:
@SomeAnnotation (1)
void someMethod() {
// ...
}
@SomeAnnotation (2)
class SomeClass {}
@SomeAnnotation String var (3)
1 | @SomeAnnotation 应用于 someMethod 方法 |
2 | @SomeAnnotation 应用于 SomeClass 类 |
3 | @SomeAnnotation 应用于 var 变量 |
为了限制注解可以应用的范围,有必要在注解定义上声明 Target
注解。例如,以下是如何声明可以将注解应用于类或方法:
import java.lang.annotation.ElementType
import java.lang.annotation.Target
@Target([ElementType.METHOD, ElementType.TYPE]) (1)
@interface SomeAnnotation {} (2)
1 | @Target 注解用于注释带有范围的注解。 |
2 | 因此,只允许在 TYPE 或 METHOD 上使用 @SomeAnnotation |
ElementType枚举中提供了可能的target列表。
Groovy不支持Java 8中引入的 TYPE_PARAMETER和 TYPE_USE元素类型。
注解成员值
使用注解时,至少需要设置所有没有默认值的成员。例如:
@interface Page {
int statusCode()
}
@Page(statusCode=404)
void notFound() {
// ...
}
但是,如果成员 value
是唯一被设置的值,则可以在注解值的声明中省略 value=
:
@interface Page {
String value()
int statusCode() default 200
}
@Page(value='/home') (1)
void home() {
// ...
}
@Page('/users') (2)
void userList() {
// ...
}
@Page(value='error',statusCode=404) (3)
void notFound() {
// ...
}
1 | 我们可以省略 statusCode ,因为它有一个默认值,但需要设置 value |
2 | 因为 value 是没有默认值的唯一需要强制设置的成员,所以我们可以省略 value= |
3 | 如果需要设置 value 和 statusCode ,则对于默认 value 成员需要使用 value= |
保留策略
注解的可见性取决于其保留策略。使用 Retention
注解设置注解的保留策略:
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
@Retention(RetentionPolicy.SOURCE) (1)
@interface SomeAnnotation {} (2)
1 | @Retention 注解注释 @SomeAnnotation 注解 |
2 | 因此 @SomeAnnotation 将保留 SOURCE |
RetentionPolicy枚举中提供了可能的保留目标和描述的列表。如何选择通常取决于你是否希望注解在编译时或运行时可见。
闭包注解参数
Groovy中注解的一个有趣特性是你可以使用闭包作为注解值。因此,注解可以与各种表达式一起使用,并且仍然具有IDE支持。例如,想象一下你希望基于JDK版本或OS等环境约束执行某些方法的框架。可以编写以下代码:
class Tasks {
Set result = []
void alwaysExecuted() {
result << 1
}
@OnlyIf({ jdk>=6 })
void supportedOnlyInJDK6() {
result << 'JDK 6'
}
@OnlyIf({ jdk>=7 && windows })
void requiresJDK7AndWindows() {
result << 'JDK 7 Windows'
}
}
要使 @OnlyIf
注解接受 Closure
作为参数,你只需将 value
声明为 Class
:
@Retention(RetentionPolicy.RUNTIME)
@interface OnlyIf {
Class value() (1)
}
为了完成该示例,让我们编写一个使用该注解的示例运行器:
class Runner {
static <T> T run(Class<T> taskClass) {
def tasks = taskClass.newInstance() (1)
def params = [jdk:6, windows: false] (2)
tasks.class.declaredMethods.each { m -> (3)
if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) { (4)
def onlyIf = m.getAnnotation(OnlyIf) (5)
if (onlyIf) {
Closure cl = onlyIf.value().newInstance(tasks,tasks) (6)
cl.delegate = params (7)
if (cl()) { (8)
m.invoke(tasks) (9)
}
} else {
m.invoke(tasks) (10)
}
}
}
tasks (11)
}
}
1 | 创建作为参数传递的类的新实例(任务类) |
2 | 模拟JDK 6且非Windows的环境 |
3 | 迭代任务类的所有声明的方法 |
4 | 如果方法是公开的并且无参数 |
5 | 尝试找到 @OnlyIf 注解 |
6 | 如果找到它获取 value 并从中创建一个新的 Closure |
7 | 将闭包的 delegate 设置为我们的环境变量 |
8 | 调用闭包,这是注解闭包。它将返回一个 boolean 值 |
9 | 如果是 true ,则调用方法 |
10 | 如果该方法未使用 @OnlyIf 注解,则无论如何都要执行该方法 |
11 | 之后,返回任务对象 |
然后Runner可以这样使用:
def tasks = Runner.run(Tasks)
assert tasks.result == [1, 'JDK 6'] as Set
元注解
声明元注解
元注解(也称为注解别名)是在编译时由其他注解替换的注解(一个元注解是一个或多个注解的别名)。元注解可用于减少涉及多个注解的代码的大小。
让我们从一个简单的例子开始。想象一下,你有 @Service
和 @Transactional
注解,并且你想用两者注解一个类:
@Service
@Transactional
class MyTransactionalService {}
考虑到可以添加到同一个类的注解的倍增,元注解可以通过使用具有完全相同语义的单个注解来减少两个注解。例如,我们可能想要写这个:
@TransactionalService (1)
class MyTransactionalService {}
1 | @TransactionalService 是一个元注解 |
元注解被声明为常规注解,但使用 @AnnotationCollector
和它正在收集的注解列表进行注解。在我们的例子中,可以编写 @TransactionalService
注解:
@Service (1)
@Transactional (2)
@AnnotationCollector (3)
@interface TransactionalService {
}
1 | 使用 @Service 注解元注解 |
2 | 使用 @Transactional 注解元注解 |
3 | 使用 @AnnotationCollector 注解元注解 |
元注解的行为
Groovy支持预编译和源表单元注解。这意味着你的元注解可能已预编译,或者你可以将其与你当前正在编译的源代码树放在同一个源代码树中。
元注解是仅限Groovy的功能。你没有机会使用元注解来注释Java类,并希望它与Groovy中的相同。同样,你不能在Java中编写元注解:元注解定义和使用都必须是Groovy代码。但是,你可以愉快地在元注解中收集Java注解和Groovy注解。 |
当Groovy编译器遇到使用元注解注释的类时,它会将其替换为收集的注解。因此,在我们之前的示例中,它将使用 @Transactional
和 @Service
替换 @TransactionalService
:
def annotations = MyTransactionalService.annotations*.annotationType()
assert (Service in annotations)
assert (Transactional in annotations)
在语义分析编译阶段期间执行从元注解到收集的注解的转换。
除了用收集的注解替换别名之外,元注解还能够处理它们,包括参数。
元注解参数
元注解可以收集具有参数的注解。为了说明这一点,我们将设想两个注解,每个注解都接受一个参数:
@Timeout(after=3600)
@Dangerous(type='explosive')
并且假设你想要创建名为 @Explosive
的元注解:
@Timeout(after=3600)
@Dangerous(type='explosive')
@AnnotationCollector
public @interface Explosive {}
默认情况下,当替换注解时,它们将获得在别名中定义的注解参数值。更有趣的是,元注解支持覆盖特定值:
@Explosive(after=0) (1)
class Bomb {}
1 | 作为 @Explosive 参数提供的 after 值会覆盖 @Timeout 注解中定义的值 |
如果两个注解定义相同的参数名称,则默认处理器会将注解值复制到接受此参数的所有注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
String value() (1)
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Bar {
String value() (2)
}
@Foo
@Bar
@AnnotationCollector
public @interface FooBar {} (3)
@Foo('a')
@Bar('b')
class Bob {} (4)
assert Bob.getAnnotation(Foo).value() == 'a' (5)
println Bob.getAnnotation(Bar).value() == 'b' (6)
@FooBar('a')
class Joe {} (7)
assert Joe.getAnnotation(Foo).value() == 'a' (8)
println Joe.getAnnotation(Bar).value() == 'a' (9)
1 | @Foo 注解定义 String 类型的 value 成员 |
2 | @Bar 注解也定义 String 类型的 value 成员 |
3 | @FooBar 元注解聚合 @Foo 和 @Bar |
4 | 类 Bob 用 @Foo 和 @Bar 注解 |
5 | Bob 上 @Foo 注解的值是 a |
6 | 而 Bob 上 @Bar 注解的值是 b |
7 | 类 Joe 使用 @FooBar 进行注解 |
8 | Joe 上 @Foo 注解的值是 a |
9 | 而 Joe 上 @Bar 注解的值也是 a |
在第二种情况下,元注解值被复制到 @Foo
和 @Bar
注解中。
如果收集的注解定义具有不兼容类型的相同成员,则为编译时错误。例如,如果在前面的示例中 @Foo
定义了 String
类型的值,但 @Bar
定义了 int
类型的值。
但是,可以自定义元注解的行为,并描述如何扩展收集的注解。我们稍后会看看如何做到这一点,但首先要有一个高级处理选项来涵盖。
处理重复注解
@AnnotationCollector
注解支持 mode
参数,该参数可用于更改默认处理器在存在重复注解时如何处理注解替换。
自定义处理器(下面讨论)可能支持也可能不支持此参数。 |
例如,假设你创建了一个包含 @ToString
注解的元注解,然后将元注解放在已经具有显式 @ToString
注解的类上。这应该是一个错误吗?是否应该同时应用这两个注解?一个优先于另一个吗?没有正确的答案。在某些情况下,任何这些答案都可能是正确的。因此,不是试图抢占一个正确的方法来处理重复的注解问题,Groovy让你编写自己的自定义元注解处理器(下面介绍),让你在AST转换中编写你喜欢的任何检查逻辑 - 这是聚合的常用目标。话虽如此,通过简单地设置 mode
,可以在任何额外的编码中自动处理许多通常预期的场景。mode
参数的行为由所选的 AnnotationCollectorMode
枚举值确定,并在下表中进行了总结。
模式 |
说明 |
DUPLICATE |
始终会插入注解集合中的注解。运行所有转换后,如果存在多个注解(不包括具有 |
PREFER_COLLECTOR |
将添加来自收集器的注解,并将删除具有相同名称的任何现有注解。 |
PREFER_COLLECTOR_MERGED |
将添加来自收集器的注解,并且将删除具有相同名称的任何现有注解,但现有注解中找到的任何新参数将合并到添加的注解中。 |
PREFER_EXPLICIT |
如果找到任何具有相同名称的现有注解,则将忽略来自收集器的注解。 |
PREFER_EXPLICIT_MERGED |
如果找到任何具有相同名称的现有注解,则会忽略来自收集器的注解,但收集器注解上的任何新参数都将添加到现有注解中。 |
自定义注解处理器
自定义注解处理器将允许你选择如何将元注解扩展为收集的注解。在这种情况下,元注解的行为完全取决于你。要做到这一点,你必须:
-
创建一个元注解处理器,继承自
AnnotationCollectorTransform
-
在元注解中声明要使用的处理器
为了说明这一点,我们将探讨如何实现元注解 @CompileDynamic
。
@CompileDynamic
是一个元注解,它将自身扩展为 @CompileStatic(TypeCheckingMode.SKIP)
。问题是默认的元注解处理器不支持枚举,注解值 TypeCheckingMode.SKIP
是其中之一。
这里天真的实现是行不通的:
@CompileStatic(TypeCheckingMode.SKIP)
@AnnotationCollector
public @interface CompileDynamic {}
相反,我们将这样定义:
@AnnotationCollector(processor = "org.codehaus.groovy.transform.CompileDynamicProcessor")
public @interface CompileDynamic {
}
你可能会注意到的第一件事是我们的注解不再使用 @CompileStatic
进行标注。这样做的原因是我们依赖于 processor
参数,它引用了一个将生成注解的类。
以下是自定义处理器的实现方式:
@CompileStatic (1)
class CompileDynamicProcessor extends AnnotationCollectorTransform { (2)
private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic) (3)
private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode) (4)
List<AnnotationNode> visit(AnnotationNode collector, (5)
AnnotationNode aliasAnnotationUsage, (6)
AnnotatedNode aliasAnnotated, (7)
SourceUnit source) { (8)
def node = new AnnotationNode(CS_NODE) (9)
def enumRef = new PropertyExpression(
new ClassExpression(TC_NODE), "SKIP") (10)
node.addMember("value", enumRef) (11)
Collections.singletonList(node) (12)
}
}
1 | 我们的自定义处理器是用Groovy编写的,为了更好的编译性能,我们使用静态编译 |
2 | 自定义处理器必须继承自 AnnotationCollectorTransform |
3 | 创建一个表示 @CompileStatic 注解类型的类节点 |
4 | 创建一个表示 TypeCheckingMode 枚举类型的类节点 |
5 | collector 是元注解中的 @AnnotationCollector 节点。通常未使用。 |
6 | aliasAnnotationUsage 是要扩展的元注解,这里是 @CompileDynamic |
7 | aliasAnnotated 是使用元注解进行注释的节点 |
8 | sourceUnit 是正在编译的 SourceUnit |
9 | 我们为 @CompileStatic 创建一个新的注解节点 |
10 | 我们创建一个等同于 TypeCheckingMode.SKIP 的表达式 |
11 | 我们将该表达式添加到注解节点,现在是 @CompileStatic(TypeCheckingMode.SKIP) |
12 | 返回生成的注解 |
在示例中,visit
方法是唯一必须重写的方法。它旨在返回将添加到使用元注解注释的节点的注解节点列表。在这个例子中,我们返回一个对应于 @CompileStatic(TypeCheckingMode.SKIP)
的单个注解。
4.1.8. 继承
(TBD)
4.1.9. 泛型
(TBD)
4.2. 特征
特征是语言的结构构造,允许:
-
行为构成
-
接口的运行时实现
-
行为重载
-
兼容静态类型的检查和编译
它们可以被视为承载默认实现和状态的接口。使用 trait
关键字定义特征:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | 声明特征 |
2 | 声明特征内的方法 |
然后它可以像使用 implements
关键字的普通接口一样使用:
class Bird implements FlyingAbility {} (1)
def b = new Bird() (2)
assert b.fly() == "I'm flying!" (3)
1 | 将特征 FlyingAbility 添加到 Bird 类功能中 |
2 | 实例化一个新的 Bird |
3 | Bird 类自动获取 FlyingAbility 特性的行为 |
特征允许从简单的组合到测试的各种功能,本节将对此进行详细介绍。
4.2.1. 方法
公共方法
声明特征中的方法可以像类中的任何常规方法一样:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | 声明特征 |
2 | 声明特征内的方法 |
抽象方法
另外,特征也可以声明抽象方法,因此需要在实现特征的类中实现:
trait Greetable {
abstract String name() (1)
String greeting() { "Hello, ${name()}!" } (2)
}
1 | 实现类必须声明 name 方法 |
2 | 可以与具体方法混合使用 |
然后可以像这样使用特征:
class Person implements Greetable { (1)
String name() { 'Bob' } (2)
}
def p = new Person()
assert p.greeting() == 'Hello, Bob!' (3)
1 | 实现特征 Greetable |
2 | 由于 name 是抽象的,因此需要实现它 |
3 | 然后可以调用 greeting |
私有方法
特征也可以定义私有方法。这些方法不会出现在特征接口契约中:
trait Greeter {
private String greetingMessage() { (1)
'Hello from a private method!'
}
String greet() {
def m = greetingMessage() (2)
println m
m
}
}
class GreetingMachine implements Greeter {} (3)
def g = new GreetingMachine()
assert g.greet() == "Hello from a private method!" (4)
try {
assert g.greetingMessage() (5)
} catch (MissingMethodException e) {
println "greetingMessage is private in trait"
}
1 | 在特征中定义私有方法 greetingMessage |
2 | 公共的 greet 消息默认调用 greetingMessage |
3 | 创建一个实现特征的类 |
4 | 调用 greet |
5 | 但不能调用 greetingMessage |
Final方法
如果我们有一个实现特征的类,概念上来自特征方法的实现被“继承”到类中。但实际上,没有包含此类实现的基类。相反,它们直接织入进类中。方法的final修饰符仅指示编织方法的修饰符。虽然允许使用相同的签名继承和覆盖或多次继承方法,但混合了final和non-final变体的方法可能被认为是糟糕的风格,但是Groovy并不禁止这种情况。应用常规方法选择,使用的修饰符将从结果方法中确定。如果你想要无法覆盖的特征实现方法,你可以考虑创建一个实现所需特征的基类。
4.2.2. this的含义
this
代表了实现的实例。把特征想象成一个超类。这意味着当你写:
trait Introspector {
def whoAmI() { this }
}
class Foo implements Introspector {}
def foo = new Foo()
然后调用:
foo.whoAmI()
将返回相同的实例:
assert foo.whoAmI().is(foo)
4.2.3. 接口
Traits可以实现接口,在这种情况下,接口是使用 implements
关键字声明的:
interface Named { (1)
String name()
}
trait Greetable implements Named { (2)
String greeting() { "Hello, ${name()}!" }
}
class Person implements Greetable { (3)
String name() { 'Bob' } (4)
}
def p = new Person()
assert p.greeting() == 'Hello, Bob!' (5)
assert p instanceof Named (6)
assert p instanceof Greetable (7)
1 | 声明一个正常接口 |
2 | 添加 Named 到已实现接口的列表 |
3 | 声明一个实现 Greetable 特征的类 |
4 | 实现缺少的 name 方法 |
5 | greeting 的实现来自特征 |
6 | 确保 Person 实现 Named 接口 |
7 | 确保 Person 实现 Greetable 特征 |
4.2.4. 属性
特征可以定义属性,如下例所示:
trait Named {
String name (1)
}
class Person implements Named {} (2)
def p = new Person(name: 'Bob') (3)
assert p.name == 'Bob' (4)
assert p.getName() == 'Bob' (5)
1 | 在特征中声明 name 属性 |
2 | 声明一个实现特征的类 |
3 | 该属性自动可见 |
4 | 它可以使用常规属性访问器访问 |
5 | 或使用常规的getter语法 |
4.2.5. 字段
私有字段
由于特征允许使用私有方法,因此使用私有字段来存储状态也很有趣。你可以这么做:
trait Counter {
private int count = 0 (1)
int count() { count += 1; count } (2)
}
class Foo implements Counter {} (3)
def f = new Foo()
assert f.count() == 1 (4)
assert f.count() == 2
1 | 在特征中声明私有字段 count |
2 | 声明一个公共方法 count ,它递增计数器并返回它 |
3 | 声明一个实现 Counter 特征的类 |
4 | count 方法可以使用私有字段来保持状态 |
这是与 Java 8接口默认方法的主要区别。虽然默认方法不带状态,但特征可以。此外,从Java 6开始就支持Groovy中的特性,因为它们的实现不依赖于默认方法。这意味着即使从Java类可以看到特征作为常规接口,该接口也不会有默认方法,只有抽象方法。
公共字段
公共字段的工作方式与私有字段相同,但为了避免 菱形问题,字段名称将在实现类中重新映射:
trait Named {
public String name (1)
}
class Person implements Named {} (2)
def p = new Person() (3)
p.Named__name = 'Bob' (4)
1 | 在特征中声明一个公共字段 |
2 | 声明一个实现特征的类 |
3 | 创建该类的实例 |
4 | 公共字段可用,但已重命名 |
字段的名称取决于特征的完全限定名称。包中的所有点(.
)都用下划线(_
)替换,最后的字段名称包含双下划线。因此,如果字段的类型是 String
,则包的名称是 my.package
,特征的名称是 Foo
,字段的名称是 bar
,在实现类中,公共字段将显示为:
String my_package_Foo__bar
虽然特征支持公共字段,但不建议使用它们并将其视为不良做法。
4.2.6. 行为的构成
特征可以被用来以一种可控的方式实现多重继承。例如,我们可以具有以下特征:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
trait SpeakingAbility {
String speak() { "I'm speaking!" }
}
还有一个实现这两个特征的类:
class Duck implements FlyingAbility, SpeakingAbility {} (1)
def d = new Duck() (2)
assert d.fly() == "I'm flying!" (3)
assert d.speak() == "I'm speaking!" (4)
1 | Duck 类实现了 FlyingAbility 和 SpeakingAbility |
2 | 创建一个新的 Duck 实例 |
3 | 我们可以从 FlyingAbility 调用方法 fly |
4 | 而且也可以从 SpeakingAbility 调用方法 speak |
Traits鼓励在对象之间重用功能,并通过现有行为的组合创建新类。
4.2.7. 重载默认方法
Traits为方法提供默认实现,但可以在实现类中覆盖它们。例如,我们可以略微改变上面的例子,有一个嘎嘎叫的鸭子:
class Duck implements FlyingAbility, SpeakingAbility {
String quack() { "Quack!" } (1)
String speak() { quack() } (2)
}
def d = new Duck()
assert d.fly() == "I'm flying!" (3)
assert d.quack() == "Quack!" (4)
assert d.speak() == "Quack!" (5)
1 | 定义一个特定于 Duck 的方法,名为 quack |
2 | 覆盖 speak 的默认实现,以便使用 quack 代替 |
3 | 从默认实现中,鸭子仍在飞行 |
4 | quack 来自 Duck 类 |
5 | 不再使用 SpeakingAbility 的默认实现 speak |
4.2.8. 继承特征
简单继承
特征可以继承另一个特征,在这种情况下,你必须使用 extends
关键字:
trait Named {
String name (1)
}
trait Polite extends Named { (2)
String introduce() { "Hello, I am $name" } (3)
}
class Person implements Polite {}
def p = new Person(name: 'Alice') (4)
assert p.introduce() == 'Hello, I am Alice' (5)
1 | Named 特征定义单个 name 属性 |
2 | Polite 特征继承了 Named 特征 |
3 | Polite 添加了一个新方法,可以访问超级特征的 name 属性 |
4 | name 属性在实现 Polite 的 Person 类中可见 |
5 | 就像 introduce 方法一样 |
多重继承
或者,特征可以继承多个其它特征。在这种情况下,必须在 implements
子句中声明所有超级特征:
trait WithId { (1)
Long id
}
trait WithName { (2)
String name
}
trait Identified implements WithId, WithName {} (3)
1 | WithId 特征定义 id 属性 |
2 | WithName 特征定义 name 属性 |
3 | Identified 是一个继承 WithId 和 WithName 的特征 |
4.2.9. 鸭子类型和特征
动态代码
Traits可以调用任何动态代码,就像普通的Groovy类一样。这意味着你可以在方法体中调用应该存在于实现类中的方法,而无需在接口中显式声明它们。这意味着traits与duck typing完全兼容:
trait SpeakingDuck {
String speak() { quack() } (1)
}
class Duck implements SpeakingDuck {
String methodMissing(String name, args) {
"${name.capitalize()}!" (2)
}
}
def d = new Duck()
assert d.speak() == 'Quack!' (3)
1 | SpeakingDuck 期望定义 quack 方法 |
2 | Duck 类确实使用 methodMissing 实现了该方法 |
3 | 调用 speak 方法会触发一个由 methodMissing 处理的 quack 调用 |
特征中的动态方法
特征也可以实现MOP方法,例如 methodMissing
或 propertyMissing
,在这种情况下,实现类将继承特征的行为,如下例所示:
trait DynamicObject { (1)
private Map props = [:]
def methodMissing(String name, args) {
name.toUpperCase()
}
def propertyMissing(String prop) {
props[prop]
}
void setProperty(String prop, Object value) {
props[prop] = value
}
}
class Dynamic implements DynamicObject {
String existingProperty = 'ok' (2)
String existingMethod() { 'ok' } (3)
}
def d = new Dynamic()
assert d.existingProperty == 'ok' (4)
assert d.foo == null (5)
d.foo = 'bar' (6)
assert d.foo == 'bar' (7)
assert d.existingMethod() == 'ok' (8)
assert d.someMethod() == 'SOMEMETHOD' (9)
1 | 创建一个实现几个MOP方法的特征 |
2 | Dynamic 类定义了一个属性 |
3 | Dynamic 类定义了一个方法 |
4 | 调用现有属性将从 Dynamic 调用该方法 |
5 | 调用不存在的属性将从特征中调用 propertyMissing 方法 |
6 | 将调用在特征上定义的 setProperty |
7 | 将调用在特征上定义的 getProperty |
8 | 在 Dynamic 上调用现有方法 |
9 | 调用一个不存在的方法出发特征上的 methodMissing |
4.2.10. 多继承冲突
默认冲突解决方案
一个类可以实现多个特征。如果某个特征定义了一个方法与另一个特征中的方法具有相同的签名,那么我们就会发生冲突:
trait A {
String exec() { 'A' } (1)
}
trait B {
String exec() { 'B' } (2)
}
class C implements A,B {} (3)
1 | trait A 定义一个名为 exec 的方法,返回一个 String |
2 | trait B 定义了相同的方法 |
3 | C 类实现两种特征 |
在这种情况下,默认行为是 implements
子句中最后声明的trait的方法获胜。这里,B
在 A
之后声明,所以 B
的方法将被选中:
def c = new C()
assert c.exec() == 'B'
用户冲突解决
如果此行为不是你想要的行为,你可以使用 Trait.super.foo
语法显式选择要调用的方法。在上面的例子中,我们可以通过写这个来确保调用特征 A
的方法:
class C implements A,B {
String exec() { A.super.exec() } (1)
}
def c = new C()
assert c.exec() == 'A' (2)
1 | 明确从特征 A 中调用 exec |
2 | 从 A 调用版本,而不是使用默认的解决方案,即从 B 调用版本 |
4.2.11. 运行时实现特征
在运行时实现特征
Groovy还支持在运行时动态实现特征。它允许你使用特征“装饰”现有对象。举个例子,让我们从这个特征和下面的类开始:
trait Extra {
String extra() { "I'm an extra method" } (1)
}
class Something { (2)
String doSomething() { 'Something' } (3)
}
1 | Extra 特征定义了一个 extra 的方法 |
2 | Something 类没有实现 Extra 特征 |
3 | Something 只定义了一个方法 doSomething |
如果我们这样做:
def s = new Something()
s.extra()
对 extra
的调用会失败,因为 Something
没有实现 Extra
。可以使用以下语法在运行时执行此操作:
def s = new Something() as Extra (1)
s.extra() (2)
s.doSomething() (3)
1 | 使用 as 关键字在运行时将对象强制转换为特征 |
2 | 然后可以在对象上调用 extra |
3 | 并且 doSomething 仍然可以调用 |
将对象强制转换为特征时,操作的结果不是同一个实例。保证强转对象将实现原始对象实现的特征和接口,但结果将不是原始类的实例。
一次实现多个特征
如果你需要一次实现多个特征,可以使用 withTraits
方法而不是 as
关键字:
trait A { void methodFromA() {} }
trait B { void methodFromB() {} }
class C {}
def c = new C()
c.methodFromA() (1)
c.methodFromB() (2)
def d = c.withTraits A, B (3)
d.methodFromA() (4)
d.methodFromB() (5)
1 | 对 methodFromA 的调用将失败,因为 C 没有实现 A |
2 | 对 methodFromB 的调用将失败,因为 C 没有实现 B |
3 | withTrait 将 c 包装成实现 A 和 B 的东西 |
4 | methodFromA 调用现在将通过,因为 d 实现了 A |
5 | methodFromB 调用现在将通过,因为 d 也实现了 B |
将对象强制转换为多个特征时,操作的结果不是同一个实例。保证强转对象将实现原始对象实现的特征和接口,但结果将不是原始类的实例。
4.2.12. 链接行为
Groovy支持可堆叠特征的概念。如果当前特征不能处理消息,则从一个特征委托给另一个特征。为了说明这一点,让我们设想一个像这样的消息处理程序接口:
interface MessageHandler {
void on(String message, Map payload)
}
然后,你可以通过应用小行为来组成消息处理程序。例如,让我们以特征的形式定义一个默认处理程序:
trait DefaultHandler implements MessageHandler {
void on(String message, Map payload) {
println "Received $message with payload $payload"
}
}
然后任何类都可以通过实现trait继承默认处理程序的行为:
class SimpleHandler implements DefaultHandler {}
现在,除了默认处理程序之外,如果要记录所有消息,该怎么办?一种选择是这样写:
class SimpleHandlerWithLogging implements DefaultHandler {
void on(String message, Map payload) { (1)
println "Seeing $message with payload $payload" (2)
DefaultHandler.super.on(message, payload) (3)
}
}
1 | 显式实现 on 方法 |
2 | 执行日志记录 |
3 | 继续委派 DefaultHandler 特征调用 |
这有效,但这种方法有缺点:
-
日志记录逻辑绑定到“具体”处理程序
-
我们在
on
方法中有一个对DefaultHandler
的显式引用,这意味着如果我们碰巧改变了类实现的特征,代码就会被破坏
作为替代方案,我们可以编写另一个特征,其责任仅限于记录日志:
trait LoggingHandler implements MessageHandler { (1)
void on(String message, Map payload) {
println "Seeing $message with payload $payload" (2)
super.on(message, payload) (3)
}
}
1 | 日志处理程序本身就是一个处理程序 |
2 | 打印它收到的消息 |
3 | 然后 super 使它将调用委托给链中的下一个特征 |
然后我们的类可以重写为:
class HandlerWithLogger implements DefaultHandler, LoggingHandler {}
def loggingHandler = new HandlerWithLogger()
loggingHandler.on('test logging', [:])
将打印:
Seeing test logging with payload [:]
Received test logging with payload [:]
由于优先级规则意味着 LoggerHandler
因为最后声明而获胜,因此对 on
的调用将使用 LoggingHandler
中的实现。但后者有一个 super
调用,这意味着委托给链中的下一个特征。这里,下一个特性是 DefaultHandler
,因此两者都将被调用:
如果我们添加第三个处理程序,这个方法将更有趣,该处理程序负责处理以 say
开头的消息:
trait SayHandler implements MessageHandler {
void on(String message, Map payload) {
if (message.startsWith("say")) { (1)
println "I say ${message - 'say'}!"
} else {
super.on(message, payload) (2)
}
}
}
1 | 处理程序特定的前提条件 |
2 | 如果不满足前提条件,则将消息传递给链中的下一个处理程序 |
然后我们的最终处理程序如下所示:
class Handler implements DefaultHandler, SayHandler, LoggingHandler {}
def h = new Handler()
h.on('foo', [:])
h.on('sayHello', [:])
意味着:
-
消息将首先通过日志记录处理程序
-
日志记录处理程序调用
super
,它将委托给下一个处理程序,即SayHandler
-
如果消息以
say
开头,那么处理程序将使用该消息 -
如果不是,
say
处理程序委托给链中的下一个处理程序
这种方法非常强大,因为它允许你编写彼此不了解的处理程序,并允许你按照所需的顺序组合它们。例如,如果我们执行代码,它将打印:
Seeing foo with payload [:]
Received foo with payload [:]
Seeing sayHello with payload [:]
I say Hello!
但是如果我们将日志记录处理程序移动到链中的第二个,则输出是不同的:
class AlternateHandler implements DefaultHandler, LoggingHandler, SayHandler {}
h = new AlternateHandler()
h.on('foo', [:])
h.on('sayHello', [:])
将打印:
Seeing foo with payload [:]
Received foo with payload [:]
I say Hello!
原因是现在,由于 SayHandler
在不调用 super
的情况下使用了消息,因此不再调用日志记录处理程序。
特征内部的super语义
如果一个类实现了多个特征并且调用了一个不合格的 super
,那么:
-
如果该类实现了另一个特征,则该调用将委托给链中的下一个特征
-
如果链中没有任何特征,
super
指的是实现类的超类(this)
例如,由于这种行为可用于装饰final类:
trait Filtering { (1)
StringBuilder append(String str) { (2)
def subst = str.replace('o','') (3)
super.append(subst) (4)
}
String toString() { super.toString() } (5)
}
def sb = new StringBuilder().withTraits Filtering (6)
sb.append('Groovy')
assert sb.toString() == 'Grvy' (7)
1 | 定义一个名为 Filtering 的特性,会在运行时应用于 StringBuilder |
2 | 重新定义 append 方法 |
3 | 从字符串中删除所有 'o' |
4 | 然后委托给 super |
5 | 如果调用 toString ,则委托给 super.toString |
6 | StringBuilder 实例上的 Filtering 特征的运行时实现 |
7 | 已追加的字符串将不再包含字母 o |
在这个例子中,当遇到 super.append
时,目标对象没有实现其他特性,因此被调用的方法是原始的 append
方法,也就是 StringBuilder
中的方法。toString
使用相同的技巧,因此将生成的代理对象的字符串表示形式委托给 StringBuilder
实例的 toString
。
4.2.13. 高级特性
SAM类型强转
如果特征定义了单个抽象方法,则它是SAM(单一抽象方法)类型强转的候选者。例如,想象以下特征:
trait Greeter {
String greet() { "Hello $name" } (1)
abstract String getName() (2)
}
1 | greet 方法不是抽象的,并调用抽象方法 getName |
2 | getName 是一个抽象方法 |
由于 getName
是 Greeter
特征中的单个抽象方法,因此你可以编写:
Greeter greeter = { 'Alice' } (1)
1 | 闭包“成为” getName 单个抽象方法的实现 |
甚至:
void greet(Greeter g) { println g.greet() } (1)
greet { 'Alice' } (2)
1 | great 方法接受 SAM 类型 Greeter 作为参数 |
2 | 我们可以用一个闭包直接调用它 |
与Java 8默认方法的差异
在Java 8中,接口可以具有方法的默认实现。如果类实现了接口并且没有为默认方法提供实现,则选择接口的实现。特征行为相同但有一个主要区别:如果类在其接口列表中声明特征并且 无论超类有没有提供实现,则始终使用特征的实现。
如果要覆盖已实现的方法的行为,可以使用此功能以非常精确的方式组合行为。
为了说明这个概念,让我们从这个简单的例子开始:
import groovy.transform.CompileStatic
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
class SomeTest extends GroovyTestCase {
def config
def shell
void setup() {
config = new CompilerConfiguration()
shell = new GroovyShell(config)
}
void testSomething() {
assert shell.evaluate('1+1') == 2
}
void otherTest() { /* ... */ }
}
在这个例子中,我们创建了一个简单的测试用例,它使用两个属性(config
和 shell
)并在多个测试方法中使用它们。现在假设你想要测试相同的东西,但使用另一个不同的编译器配置。一种选择是创建 SomeTest
的子类:
class AnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
它可以工作,但是如果你实际上有多个测试类,并且你想测试所有这些测试类的新配置呢?然后,你必须为每个测试类创建一个不同的子类:
class YetAnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
然后你看到的是两个测试的 setup
方法是相同的。那么,另一种选择是创建一个特征:
trait MyTestSupport {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( new ASTTransformationCustomizer(CompileStatic) )
shell = new GroovyShell(config)
}
}
然后在子类中使用它:
class AnotherTest extends SomeTest implements MyTestSupport {}
class YetAnotherTest extends SomeTest2 implements MyTestSupport {}
...
这将允许我们大幅减少样板代码,并降低忘记更改 setup
代码的风险,以防我们决定更改它。即使 setup
已经在超类中实现,由于测试类在其接口列表中声明了特征,因此行为将从特征实现中借用!
当你无权访问超类源代码时,此功能特别有用。它可以用于mock方法或强制子类中方法的特定实现。它允许你重构代码以将重写的逻辑保留在单个特征中,并通过实现它来继承新行为。当然,另一种方法是在你使用新代码的每个地方覆盖该方法。
值得注意的是,如果使用运行时特征,则特征中的方法始终优先于代理对象的方法:
class Person {
String name (1)
}
trait Bob {
String getName() { 'Bob' } (2)
}
def p = new Person(name: 'Alice')
assert p.name == 'Alice' (3)
def p2 = p as Bob (4)
assert p2.name == 'Bob' (5)
1 | Person 类定义一个 name 属性,该属性导致生成 getName 方法 |
2 | Bob 是一个将 getName 定义为返回 Bob 的特征 |
3 | 默认对象将返回 Alice |
4 | p2 在运行时强转 p 为 Bob |
5 | getName 返回 Bob,因为 getName 取自特征 |
同样,不要忘记动态特征强制返回一个新的对象,它仅实现原始接口以及特征。
4.2.14. 与mixin的差异
mixin有几个概念上的差异,因为它们在Groovy中可用。请注意,我们讨论的是运行时mixins,而不是 @Mixin
注解,它被弃用以支持traits。
首先,在字节码中可以看到特征中定义的方法:
-
在内部,特征表示为接口(没有默认或静态方法)和几个辅助类
-
这意味着实现特征的对象有效地实现了一个接口
-
这些方法在Java中是可见的
-
它们与类型检查和静态编译兼容
相反,通过mixin添加的方法仅在运行时可见:
class A { String methodFromA() { 'A' } } (1)
class B { String methodFromB() { 'B' } } (2)
A.metaClass.mixin B (3)
def o = new A()
assert o.methodFromA() == 'A' (4)
assert o.methodFromB() == 'B' (5)
assert o instanceof A (6)
assert !(o instanceof B) (7)
1 | 类 A 定义 methodFromA |
2 | 类 B 定义 methodFromB |
3 | 把 B 混合进 A |
4 | 我们可以调用 methodFromA |
5 | 我们也可以调用 methodFromB |
6 | 该对象是 A 的一个实例 |
7 | 但它不是 B 的实例 |
最后一点实际上非常重要,并说明了一个mixins比traits更有优势的地方:实例不会被修改,所以如果你将某个类混合到另一个类中,则不会生成第三个类,并且响应 A
的方法将继续响应 A
,即使它是混合的。
4.2.15. 静态方法,属性和字段
以下说明需谨慎。静态成员支持正在进行中,仍在进行实验。以下信息仅适用于2.5.6。
可以在特征中定义静态方法,但它有许多限制:
-
含静态方法的特征无法静态编译或类型检查。动态访问所有静态方法,属性和字段(这是JVM的限制)。
-
静态方法不会出现在每个特征的生成接口中。
-
特征被解释为实现类的模板,这意味着每个实现类将获得自己的静态方法,属性和字段。因此,在特征上声明的静态成员不属于特征,而是属于它的实现类。
-
你通常不应混合使用相同签名的静态和实例方法。适用于应用特征的常规规则(包括多重继承冲突解决)。如果选择的方法是静态的,但某些已实现的特征具有实例变量,则会发生编译错误。如果选择的方法是实例变量,则将忽略静态变量(对于这种情况,行为类似于Java接口中的静态方法)。
让我们从一个简单的例子开始:
trait TestHelper {
public static boolean CALLED = false (1)
static void init() { (2)
CALLED = true (3)
}
}
class Foo implements TestHelper {}
Foo.init() (4)
assert Foo.TestHelper__CALLED (5)
1 | 在特征中声明静态字段 |
2 | 在特征中也声明了静态方法 |
3 | 在特征内更新静态字段 |
4 | 静态方法 init 可用于实现类 |
5 | 重新映射静态字段以避免菱形问题 |
像往常一样,不建议使用公共字段。无论如何,如果你想要这么做,你必须明白以下代码会失败:
Foo.CALLED = true
因为在特征本身上没有定义静态字段 CALLED
。同样,如果你有两个不同的实现类,则每个类都会获得一个不同的静态字段:
class Bar implements TestHelper {} (1)
class Baz implements TestHelper {} (2)
Bar.init() (3)
assert Bar.TestHelper__CALLED (4)
assert !Baz.TestHelper__CALLED (5)
1 | 类 Bar 实现了特征 |
2 | 类 Baz 也实现了该特征 |
3 | 只在 Bar 上调用 init |
4 | Bar 上的静态字段 CALLED 已更新 |
5 | 但 Baz 上的静态字段 CALLED 不是真,因为它是互不相同的 |
4.2.16. 状态继承的陷阱
我们已经看到特征是有状态的。特征可以定义字段或属性,但是当类实现特征时,它会基于每个特征获取这些字段/属性。请考虑以下示例:
trait IntCouple {
int x = 1
int y = 2
int sum() { x+y }
}
该特征定义了两个属性 x
和 y
,以及 sum
方法。现在让我们创建一个实现特征的类:
class BaseElem implements IntCouple {
int f() { sum() }
}
def base = new BaseElem()
assert base.f() == 3
调用 f
的结果是 3
,因为委托在具有状态的特征中求和。但是如果我们这样写呢?
class Elem implements IntCouple {
int x = 3 (1)
int y = 4 (2)
int f() { sum() } (3)
}
def elem = new Elem()
1 | 覆盖属性 x |
2 | 覆盖属性 y |
3 | 从特征中调用 sum |
如果你调用 elem.f()
,预期的输出是多少?实际上它是:
assert elem.f() == 3
原因是 sum
方法访问特征的字段。所以它使用了特征中定义的 x
和 y
值。如果要使用实现类中的值,则需要使用getter和setter取消引用字段,如上一个示例所示:
rait IntCouple {
int x = 1
int y = 2
int sum() { getX()+getY() }
}
class Elem implements IntCouple {
int x = 3
int y = 4
int f() { sum() }
}
def elem = new Elem()
assert elem.f() == 7
4.2.17. 自我类型
特征的类型约束
有时你会想要写一个只能应用于某种类型的特征。例如,你可能希望在扩展另一个类的类上应用特征,这个类是你无法控制的,并且仍然可以调用这些方法。为了说明这一点,让我们从这个例子开始:
class CommunicationService {
static void sendMessage(String from, String to, String message) { (1)
println "$from sent [$message] to $to"
}
}
class Device { String id } (2)
trait Communicating {
void sendMessage(Device to, String message) {
CommunicationService.sendMessage(id, to.id, message) (3)
}
}
class MyDevice extends Device implements Communicating {} (4)
def bob = new MyDevice(id:'Bob')
def alice = new MyDevice(id:'Alice')
bob.sendMessage(alice,'secret') (5)
1 | 不可控的 Service 类(在库中,…)定义了一个 sendMessage 方法 |
2 | 不可控的 Device 类(在库中,……) |
3 | 为可以调用服务的设备定义通信特征 |
4 | 将 MyDevice 定义为通信设备 |
5 | 调用特征中的方法,并解析 id |
这里很清楚,Communicating
特征只能应用于设备。但是,没有明确的契约来表明,因为特征不能继承类。但是,代码编译和运行完全正常,因为特征方法中的 id
将动态解析。问题在于没有任何东西阻止特征被应用于任何不是 Device
的类。任何具有 id
的类都可以工作,而任何没有 id
属性的类都会导致运行时错误。
如果你想在特征上启用类型检查或应用 @CompileStatic
,问题就更复杂了:因为特征对自身不是 Device
一无所知,所以类型检查器会抱怨它没有找到 id
属性。
一种可能性是在特征中明确添加 getId
方法,但它不会解决所有问题。如果方法需要将 this
作为参数,并且实际上要求它是 Device
,该怎么办?
class SecurityService {
static void check(Device d) { if (d.id==null) throw new SecurityException() }
}
如果你希望能够在特征中调用 this
,那么你将明确需要将 this
转换为 Device
。如果到处都使用显式强制转换 this
,这将很快变得不可读。
@SelfType
注解
为了使这个契约显式化,并使类型检查器知道它自己的类型,Groovy提供了一个 @SelfType
注解,它将:
-
让你声明实现此特征的类必须继承或实现的类型
-
如果不满足这些类型约束,则抛出编译时错误
所以在前面的例子中,我们可以使用 @groovy.transform.SelfType
注解来fix特征:
@SelfType(Device)
@CompileStatic
trait Communicating {
void sendMessage(Device to, String message) {
SecurityService.check(this)
CommunicationService.sendMessage(id, to.id, message)
}
}
现在,如果你尝试在不是设备的类上实现此特征,则会发生编译时错误:
class MyDevice implements Communicating {} // 忘记继承 Device
将抛出错误:
class 'MyDevice' implements trait 'Communicating' but does not extend self type class 'Device'
总之,自我类型是一种强有力的方式来宣布对特征的约束,而不必直接在特征中声明契约或不得不在任何地方使用强制类型转换,从而保持关注点的分离尽可能的紧密。
4.2.18. 限制
与AST转换的兼容性
特征不与AST转换正式兼容。其中一些,如 @CompileStatic
将应用于特征本身(而不是实现类),而其他一些将同时应用于实现类和特征。绝对不能保证AST转换会像普通类一样在特征上运行,因此使用它需要你自担风险!
前缀和后缀操作
在特征中,如果更新特征的字段,则不允许使用前缀和后缀操作:
trait Counting {
int x
void inc() {
x++ (1)
}
void dec() {
--x (2)
}
}
class Counter implements Counting {}
def c = new Counter()
c.inc()
1 | x 在特征内定义,不允许后缀自增 |
2 | x 在特征内定义,不允许前缀自减 |
解决方法是使用 +=
运算符。
5. 闭包
本章介绍Groovy Closures。Groovy中的闭包是一个开放的,匿名的代码块,可以接受参数,返回值并赋值给变量。闭包可以引用在其周围范围内声明的变量。与闭包的正式定义相反,Groovy语言中的 Closure
也可以包含在其周围范围之外定义的自由变量。在打破闭包的正式概念的同时,它提供了本章所述的各种优点。
5.1. 语法
5.1.1. 定义一个闭包
闭包定义遵循以下语法:
{ [closureParameters -> ] statements }
其中 [closureParameters→]
是一个以逗号分隔的可选参数列表,而语句是0或多个Groovy语句。参数看起来类似于方法参数列表,这些参数可以是类型化的或非类型化的。
指定参数列表时, →
字符是必需的,用于将参数与闭包体分开。语句部分由0,1或更多的Groovy语句组成。
有效闭包定义的一些示例:
{ item++ } (1)
{ -> item++ } (2)
{ println it } (3)
{ it -> println it } (4)
{ name -> println name } (5)
{ String x, int y -> (6)
println "hey ${x} the value is ${y}"
}
{ reader -> (7)
def line = reader.readLine()
line.trim()
}
1 | 一个引用名为 item 变量的闭包 |
2 | 通过添加箭头( → )可以明确地将闭包参数与代码分开 |
3 | 使用隐式参数(it )的闭包 |
4 | 一个替代版本,it 是一个显式参数 |
5 | 在这种情况下,通常最好为参数使用显式名称 |
6 | 一个接受两个类型参数的闭包 |
7 | 包含多个语句的闭包 |
5.1.2. 闭包作为对象
闭包是 groovy.lang.Closure
类的一个实例,它可以像任何其他变量一样赋给变量或字段,尽管它是一个代码块:
def listener = { e -> println "Clicked on $e.source" } (1)
assert listener instanceof Closure
Closure callback = { println 'Done!' } (2)
Closure<Boolean> isTextFile = {
File it -> it.name.endsWith('.txt') (3)
}
1 | 你可以将闭包赋值给变量,它是 groovy.lang.Closure 的一个实例 |
2 | 如果不使用 def ,也可以将闭包赋给 groovy.lang.Closure 类型的变量 |
3 | (可选)你可以使用 groovy.lang.Closure 的泛型类型指定闭包的返回类型 |
5.1.3. 调用一个闭包
作为匿名代码块的闭包可以像任何其他方法一样被调用。如果你这样定义一个不带参数的闭包:
def code = { 123 }
然后闭包内的代码只会在你call闭包时执行,这可以通过使用变量来完成,就好像它是一个常规方法:
assert code() == 123
或者,你可以显式使用 call
方法:
assert code.call() == 123
如果闭包接受参数,原则是相同的:
def isOdd = { int i -> i%2 != 0 } (1)
assert isOdd(3) == true (2)
assert isOdd.call(2) == false (3)
def isEven = { it%2 == 0 } (4)
assert isEven(3) == false (5)
assert isEven.call(2) == true (6)
1 | 定义一个接受 int 作为参数的闭包 |
2 | 它可以直接调用 |
3 | 或使用 call 方法 |
4 | 具有隐式参数(it )的闭包也是如此 |
5 | 可以使用 (arg) 直接调用 |
6 | 或使用 call |
与方法不同,闭包在调用时始终返回一个值。下一节将讨论如何声明闭包参数,何时使用它们以及隐含的 it
参数是什么。
5.2. 参数
5.2.1. 普通参数
闭包的参数遵循与常规方法的参数相同的原则:
-
一个可选类型
-
一个名字
-
一个可选的默认值
参数用逗号分隔:
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
5.2.2. 隐含参数
当闭包没有显式定义参数列表(使用 →
)时,闭包总是定义一个名为 it
的隐式参数。这意味着这段代码:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
完全等同于这个:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果要声明一个不接受任何参数的闭包,并且必须限制为不带参数的调用,那么必须使用显式空参数列表声明它:
def magicNumber = { -> 42 }
// 此调用将失败,因为闭包不接受任何参数
magicNumber(11)
5.2.3. 可变参数
闭包可以像任何其他方法一样声明可变参数。如果最后一个参数是可变长度(或数组),Vargs方法是可以接受可变数量参数的方法,如下一个示例所示:
def concat1 = { String... args -> args.join('') } (1)
assert concat1('abc','def') == 'abcdef' (2)
def concat2 = { String[] args -> args.join('') } (3)
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> (4)
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
1 | 一个接受可变数量的字符串作为第一个参数的闭包 |
2 | 可以使用任意数量的参数调用它,而无需将它们显式地包装到数组中 |
3 | 如果将args参数声明为数组,则可以直接使用相同的行为 |
4 | 只要最后一个参数是数组或显式vargs类型 |
5.3. 委托策略
5.3.1. Groovy闭包 vs lambda表达式
Groovy将闭包定义为Closure类的实例。它使它与 Java 8中的lambda表达式截然不同。委托是Groovy闭包中的一个关键概念,它在lambda中没有等价物。能够更改委托或更改闭包的委派策略使得在Groovy中设计漂亮的域特定语言(DSL)成为可能。
5.3.2. Owner, delegate 和 this
要理解委托的概念,首先必须解释闭包中 this
的含义。闭包实际上定义了3个不同的东西:
-
this
对应于定义闭包的封闭类 -
owner
对应于定义闭包的封闭对象,该对象可以是类或闭包 -
delegate
对应于一个第三方对象,其中在未定义消息的接收者时解析方法调用或属性
this的含义
在闭包中,调用 getThisObject
将返回定义闭包的封闭类。它相当于使用一个显式的 this
:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } (1)
assert whatIsThisObject() == this (2)
def whatIsThis = { this } (3)
assert whatIsThis() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } (7)
cl()
}
assert nestedClosures() == this (8)
}
}
1 | 闭包是在 Enclosing 类中定义的,并返回 getThisObject |
2 | 调用闭包将返回定义闭包的 Enclosing 实例 |
3 | 通常,你只想使用 this 符号快捷方式 |
4 | 它返回完全相同的对象 |
5 | 如果闭包是在内部类中定义的 |
6 | this 在闭包中将返回内部类,而不是顶级类 |
7 | 在嵌套闭包的情况下,像这里 cl 被定义在 nestedClosures 闭包的范围内 |
8 | 那么 this 对应于最近的外部类,而不是封闭的闭包! |
当然可以通过这种方式从封闭类中调用方法:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() (1)
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1 | 闭包调用 this 上的 toString ,它实际上会调用封闭对象上的 toString 方法,也就是 Person 实例 |
闭包的owner
闭包的owner与闭包中的this的定义非常相似,但有一个细微的区别:它将返回直接封闭对象,无论是闭包还是类:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } (1)
assert whatIsOwnerMethod() == this (2)
def whatIsOwner = { owner } (3)
assert whatIsOwner() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } (7)
cl()
}
assert nestedClosures() == nestedClosures (8)
}
}
1 | 闭包是在 Enclosing 类中定义的,并返回 getOwner |
2 | 调用闭包将返回定义闭包的 Enclosing 实例 |
3 | 通常,你只想使用 owner 符号快捷方式 |
4 | 它返回完全相同的对象 |
5 | 如果闭包是在内部类中定义的 |
6 | owner 在闭包中将返回内部类,而不是顶级类 |
7 | 在嵌套闭包的情况下,像这里 cl 被定义在 nestedClosures 闭包的范围内 |
8 | 那么 owner 对应于封闭的闭包,因此是与 this 不同的对象! |
闭包的delegate
可以使用 delegate
属性或调用 getDelegate
方法来访问闭包的委托。它是在Groovy中构建特定于域的语言的强大概念。虽然closure-this和closure-owner引用了闭包的词法范围,但委托是一个闭包将使用的用户定义对象。默认情况下,委托设置为 owner
:
class Enclosing {
void run() {
def cl = { getDelegate() } (1)
def cl2 = { delegate } (2)
assert cl() == cl2() (3)
assert cl() == this (4)
def enclosed = {
{ -> delegate }.call() (5)
}
assert enclosed() == enclosed (6)
}
}
1 | 你可以获得一个调用 getDelegate 方法的闭包的委托 |
2 | 或使用 delegate 属性 |
3 | 两者都返回相同的对象 |
4 | 这是封闭的类或闭包 |
5 | 特别是在嵌套闭包的情况下 |
6 | delegate 将对应 owner |
闭包的委托可以更改为任何对象。让我们通过创建两个不是彼此的子类但都定义名为 name
的属性来说明这一点:
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然后让我们定义一个闭包,它在委托上获取 name
属性:
def upperCasedName = { delegate.name.toUpperCase() }
然后通过更改闭包的委托,你可以看到目标对象将更改:
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
此时,行为与在闭包的词法范围中定义一个 target
变量没有区别:
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
但是,存在重大差异:
-
在最后一个示例中,
target
是从闭包内引用的局部变量 -
委托可以透明地使用,也就是说没有使用
delegate
的方法调用前缀。如下所述。
委托策略
每当在闭包中访问属性而不显式设置接收器对象时,就会涉及委派策略:
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } (1)
cl.delegate = p (2)
assert cl() == 'IGOR' (3)
1 | name 不引用闭包的词法范围中的变量 |
2 | 我们可以将闭包的委托更改为 Person 的实例 |
3 | 并且方法调用将成功 |
此代码工作的原因是 name
属性将在 delegate
对象上透明地解析!这是解决闭包内属性或方法调用的一种非常强大的方法。无需设置显式 delegate
。接收器:将进行调用,因为闭包的默认委托策略就是这样。闭包实际上定义了多种解析策略,你可以选择:
-
Closure.OWNER_FIRST
是默认策略。如果owner上存在属性/方法,则将在所有者上调用它。如果没有,则使用delegate。 -
Closure.DELEGATE_FIRST
颠倒逻辑:首先使用delegate,然后使用owner。 -
Closure.OWNER_ONLY
将仅解析owner的属性/方法查找:delegate将被忽略。 -
Closure.DELEGATE_ONLY
将仅解析delegate的属性/方法查找:owner将被忽略。 -
Closure.TO_SELF
可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用:解决方案不会在owner或delegate上进行,而只能在闭包类本身上进行。如果你实现自己的Closure
子类,那么使用它是唯一有意义的。
让我们用这段代码说明默认的“所有者优先”策略:
class Person {
String name
def pretty = { "My name is $name" } (1)
String toString() {
pretty()
}
}
class Thing {
String name (2)
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' (3)
p.pretty.delegate = t (4)
assert p.toString() == 'My name is Sarah' (5)
1 | 为了说明,我们定义了一个引用 "name" 的闭包成员 |
2 | Person 和 Thing 类都定义了一个 name 属性 |
3 | 使用默认策略,首先在所有者上解析 name 属性 |
4 | 所以,如果我们将 delegate 更改为 t ,这是 Thing 的一个实例 |
5 | 结果没有变化:name 首先在闭包的 owner 上解析 |
但是,可以更改闭包的解决策略:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通过更改 resolveStrategy
,我们正在修改Groovy解析“隐式this”引用的方式:在这种情况下,name
将首先在委托中查找,如果没有找到,则在所有者上查找。由于 name
是在委托中定义的,一个 Thing
实例,因此使用此值。
如果委托人(resp. owner)之一没有这样的方法或属性,则可以说明“delegate first”和“delegate only”或“owner first”和“owner only”之间的差异:
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == 42
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42
cl.delegate = t
try {
cl()
assert false
} catch (MissingPropertyException ex) {
// "age" 没有被定义在 delegate 中
}
在这个例子中,我们定义了两个都有 name
属性的类,但是只有 Person
类声明了一个 age
。Person
类还声明了一个引用 age
的闭包。我们可以将默认解析策略从“owner first”更改为“delegate only”。由于闭包的所有者是 Person
类,因此我们可以检查如果委托是 Person
的实例,则调用闭包是成功的,但是如果我们使用委托作为 Thing
的实例来调用它,则它会失败并且抛出 groovy.lang.MissingPropertyException
。尽管在 Person
类中定义了闭包,但不使用所有者。
有关如何使用此功能开发DSL的全面说明,请参阅 本手册的专用部分。
5.4. GStrings中的闭包
请使用以下代码:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码的行为与你期望的一样,但是如果添加以下内容会发生什么:
x = 2
assert gs == 'x = 2'
你会看到断言失败了!有两个原因:
-
GString只是懒惰地评估值的
toString
表示 -
GString中的语法
${x}
不表示闭包,而是表示$x
的表达式,在创建GString时计算。
在我们的示例中,GString
是使用引用 x
的表达式创建的。创建 GString
时,x
的值为1,因此创建的 GString
值为1.当断言触发时,将评估 GString
,并使用 toString
将1转换为 String
。当我们将 x
更改为2时,我们确实更改了 x
的值,但它是一个不同的对象,GString
仍然引用旧的。
如果它引用的值是变化的,GString
只会更改其 toString
表示。如果引用发生变化,则不会发生任何事。
如果你需要 GString
中的真实闭包,例如强制执行惰性变量评估,则需要使用以下修复示例中的替代语法 ${→ x}
:
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
让我们用这段代码说明它与变异的区别:
class Person {
String name
String toString() { name } (1)
}
def sam = new Person(name:'Sam') (2)
def lucy = new Person(name:'Lucy') (3)
def p = sam (4)
def gs = "Name: ${p}" (5)
assert gs == 'Name: Sam' (6)
p = lucy (7)
assert gs == 'Name: Sam' (8)
sam.name = 'Lucy' (9)
assert gs == 'Name: Lucy' (10)
1 | Person 类有一个返回 name 属性的 toString 方法 |
2 | 我们创建了第一个名叫Sam的 Person |
3 | 我们创建另一个名叫Lucy的 Person |
4 | p 变量设置为 Sam |
5 | 并创建一个闭包,引用 p 的值,也就是说Sam |
6 | 所以当我们评估字符串时,它会返回Sam |
7 | 如果我们改变 p 到Lucy |
8 | 字符串仍然评估为Sam,因为它是创建 GString 时的 p 值 |
9 | 所以,如果我们改变Sam将他的名字改为Lucy |
10 | 这次 GString 被正确地改变了 |
因此,如果你不想依赖变异对象或包装对象,则必须通过显式声明空参数列表来使用 GString
中的闭包:
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// 使用惰性评估"p"创建一个GString
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
5.5. 闭包强转
可以将闭包转换为接口或单抽象方法类型。有关完整说明,请参阅本手册的这一小节。
5.6. 函数式编程
闭包,如 Java 8中的lambda表达式,是Groovy中函数式编程范例的核心。关于函数的一些函数编程操作可直接在 Closure
类上获得,如本节所示。
5.6.1. 局部应用
在Groovy中,currying是指部分应用的概念。它不符合函数式编程中currying的真实概念,因为Groovy在闭包上应用了不同的作用域规则。在Groovy中进行Currying将允许你设置闭包的一个参数的值,并且它将返回一个接受少一个参数的新闭包。
左局部应用
左局部应用是指设置闭包最左边的参数,如本例所示:
def nCopies = { int n, String str -> str*n } (1)
def twice = nCopies.curry(2) (2)
assert twice('bla') == 'blabla' (3)
assert twice('bla') == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | curry 会将第一个参数设置为 2 ,创建一个接受单个 String 的新闭包(函数) |
3 | 所以只使用 String 调用新函数调用 |
4 | 它相当于用两个参数调用 nCopies |
右局部应用
与左局部应用相似,也可以设置闭包的最右侧参数:
def nCopies = { int n, String str -> str*n } (1)
def blah = nCopies.rcurry('bla') (2)
assert blah(2) == 'blabla' (3)
assert blah(2) == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | rcurry 会将最后一个参数设置为 bla ,创建一个接受单个 int 的新闭包(函数) |
3 | 所以只使用 int 调用新函数调用 |
4 | 它相当于用两个参数调用 nCopies |
基于索引的局部应用
如果闭包接受超过2个参数,则可以使用 curry
设置任意位置的参数:
def volume = { double l, double w, double h -> l*w*h } (1)
def fixedWidthVolume = volume.ncurry(1, 2d) (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) (5)
1 | volume 函数定义了3个参数 |
2 | ncurry 会将第二个参数(index = 1)设置为 2d ,创建一个接受长度和高度的新体积函数 |
3 | 该功能相当于调用省略宽度参数的 volume |
4 | 也可以从指定的索引开始设置多个参数 |
5 | 结果函数接受与初始函数一样多的参数且减去 ncurry 设置的参数数量 |
5.6.2. 记忆化
Memoization允许缓存闭包调用的结果。有趣的是,函数(闭包)完成的计算速度很慢,但是你知道这个函数经常会用相同的参数调用。一个典型的例子是Fibonacci套件。一个天真的实现可能如下所示:
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!
这是一个天真的实现,因为’fib’通常使用相同的参数递归调用,从而产生指数算法:
-
计算
fib(15)
需要fib(13)
和fib(14)
的结果 -
计算
fib(14)
需要fib(12)
和fib(13)
的结果
由于调用是递归的,你已经可以看到我们将一次又一次地计算相同的值,尽管它们可以被缓存。通过使用 memoize
缓存调用结果,可以“修复”这种天真的实现:
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 // fast!
缓存使用参数的实际值进行工作。这意味着如果你使用除原始或包装基元类型之外的其他内容进行memoization,则应该非常小心。
可以使用另一些方法调整缓存的行为:
-
memoizeAtMost
将生成一个新的闭包,它最多可以缓存n个值 -
memoizeAtLeast
将生成一个新的闭包,它至少缓存n个值 -
memoizeBetween
将生成一个新的闭包,它至少缓存n个值和最多缓存n个值
所有memoize变体中使用的缓存都是LRU缓存。
5.6.3. 组合
闭包组合对应于函数组合的概念,即通过组合两个或多个函数(链接调用)来创建新函数,如下例所示:
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// 逆向组合
assert times3plus2(3) == (times3 >> plus2)(3)
5.6.4. 蹦床
递归算法通常受物理限制的限制:最大堆栈高度。例如,如果调用递归调用自身的方法太深,则最终会收到 StackOverflowException
。
在这些情况下有用的方法是使用 Closure
及其蹦床功能。
闭包包裹在 TrampolineClosure
中。在调用时,一个蹦床 Closure
会调用原来的 Closure
来等待它的结果。如果调用的结果是 TrampolineClosure
的另一个实例,也许是因为调用 trampoline()
方法而创建的,则将再次调用 Closure
。返回的蹦床Closures实例的重复调用将继续,直到返回除蹦床Closure之外的值。这个值将成为蹦床的最终结果。这样,调用是连续进行的,而不是填充堆栈。
以下是使用 trampoline()
实现阶乘函数的示例:
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits
6. 语义
本章介绍了Groovy编程语言的语义。
6.1. 语句
6.1.1. 变量定义
可以使用类型(如 String
)或使用关键字 def
定义变量:
String x
def o
def
是类型名称的替代品。在变量定义中,它用于表示你不关心类型。在变量定义中,必须明确提供类型名称或使用“def”替代。这需要使Groovy解析器可以检测到变量定义。
你可以将 def
视为 Object
的别名,这样你将很好理解它。
可以使用泛型来改进变量定义类型,例如 List <String> names
。要了解有关泛型支持的更多信息,请阅读泛型部分。
6.1.2. 变量赋值
你可以为变量赋值以供以后使用。请尝试以下方法:
x = 1
println x
x = new java.util.Date()
println x
x = -3.1499392
println x
x = false
println x
x = "Hi"
println x
多重赋值
Groovy支持多重赋值,即可以同时赋值多个变量,例如:
def (a, b, c) = [10, 20, 'foo']
assert a == 10 && b == 20 && c == 'foo'
如果你愿意,可以提供类型作为声明的一部分:
def (int i, String j) = [10, 'foo']
assert i == 10 && j == 'foo'
除了在声明变量时使用,它也适用于已有变量:
def nums = [1, 3, 5]
def a, b, c
(a, b, c) = nums
assert a == 1 && b == 3 && c == 5
该语法适用于数组和列表,以及返回其中任一类型的方法:
def (_, month, year) = "18th June 2009".split()
assert "In $month of $year" == 'In June of 2009'
上溢和下溢
如果左侧有太多变量,多余的变量用null填充:
def (a, b, c) = [1, 2]
assert a == 1 && b == 2 && c == null
如果右侧有太多变量,则会忽略额外的变量:
def (a, b) = [1, 2, 3]
assert a == 1 && b == 2
具有多重赋值的对象解构
使用这种技术,我们可以组合多重赋值和下标运算符方法来实现对象解构。
考虑以下不可变的 Coordinates
类,包含一对经度和纬度的双精度,并注意我们的 getAt()
方法的实现:
@Immutable
class Coordinates {
double latitude
double longitude
double getAt(int idx) {
if (idx == 0) latitude
else if (idx == 1) longitude
else throw new Exception("Wrong coordinate index, use 0 or 1")
}
}
现在让我们实例化这个类并解构它的经度和纬度:
def coordinates = new Coordinates(latitude: 43.23, longitude: 3.67) (1)
def (la, lo) = coordinates (2)
assert la == 43.23 (3)
assert lo == 3.67
1 | 我们创建一个 Coordinates 类的实例 |
2 | 然后,我们使用多重赋值来获取各个经度和纬度值 |
3 | 最后断言他们的值 |
6.1.3. 控制结构
条件结构
if / else
Groovy支持Java中常用的if-else语法
def x = false
def y = false
if ( !x ) {
x = true
}
assert x == true
if ( x ) {
x = false
} else {
y = true
}
assert x == y
Groovy还支持普通的Java“嵌套”if then else if语法
if ( ... ) {
...
} else if (...) {
...
} else {
...
}
switch / case
Groovy中的switch语句向后兼容Java代码;因此,你可以fall through case为多个匹配共享相同的代码。
但有一点不同的是,Groovy switch语句可以处理任何类型的switch值,并且可以执行不同类型的匹配。
def x = 1.23
def result = ""
switch ( x ) {
case "foo":
result = "found foo"
// lets fall through
case "bar":
result += "bar"
case [4, 5, 6, 'inList']:
result = "list"
break
case 12..30:
result = "range"
break
case Integer:
result = "integer"
break
case Number:
result = "number"
break
case ~/fo*/: // toString() representation of x matches the pattern?
result = "foo regex"
break
case { it < 0 }: // or { x < 0 }
result = "negative"
break
default:
result = "default"
}
assert result == "number"
Switch支持以下类型的比较:
-
如果switch值是类的实例,则类用例值匹配
-
如果switch值的
toString()
表示与正则表达式匹配,则正则表达式用例值匹配 -
如果switch值包含在集合中,则集合用例值匹配。这还包括范围(因为它们是列表)
-
如果根据Groovy truth调用闭包返回结果为true,则闭包用例值匹配
-
如果不使用上述任何一个,那么如果用例值等于switch值,则该用例值匹配
使用闭包用例值时,默认 it
参数实际上是switch值(在我们的示例中,变量 x
)。
循环结构
经典循环
Groovy支持标准的Java/C for循环:
String message = ''
for (int i = 0; i < 5; i++) {
message += 'Hi '
}
assert message == 'Hi Hi Hi Hi Hi '
for in循环
Groovy中的for循环更简单,适用于任何类型的数组,集合,Map等。
// iterate over a range
def x = 0
for ( i in 0..9 ) {
x += i
}
assert x == 45
// iterate over a list
x = 0
for ( i in [0, 1, 2, 3, 4] ) {
x += i
}
assert x == 10
// iterate over an array
def array = (0..4).toArray()
x = 0
for ( i in array ) {
x += i
}
assert x == 10
// iterate over a map
def map = ['abc':1, 'def':2, 'xyz':3]
x = 0
for ( e in map ) {
x += e.value
}
assert x == 6
// iterate over values in a map
x = 0
for ( v in map.values() ) {
x += v
}
assert x == 6
// iterate over the characters in a string
def text = "abc"
def list = []
for (c in text) {
list.add(c)
}
assert list == ["a", "b", "c"]
Groovy还支持使用冒号的Java冒号变体:for (char c : text) {}
,其中变量的类型是必需的。
while循环
Groovy支持像Java一样的 while{…}循环:
def x = 0
def y = 5
while ( y-- > 0 ) {
x++
}
assert x == 5
异常处理
异常处理与Java相同。
try / catch / finally
你可以指定一个完整的try-catch-finally,try-catch或try-finally块。
每个块体都需要大括号包围。
try {
'moo'.toLong() // 这将产生一个异常
assert false // 永远不会到达这的断言语句
} catch ( e ) {
assert e in NumberFormatException
}
我们可以在匹配的’try’子句后面的’finally’子句中放置代码,这样无论’try’子句中的代码是否抛出异常,finally子句中的代码总是会执行:
def z
try {
def i = 7, j = 0
try {
def k = i / j
assert false // 由于前一行中的异常而从未到达过
} finally {
z = 'reached here' // 即使抛出Exception也总是执行
}
} catch ( e ) {
assert e in ArithmeticException
assert z == 'reached here'
}
多重catch
使用多catch块(从Groovy 2.0开始),我们可以定义几个异常,以便捕获并由同一个catch块处理:
try {
/* ... */
} catch ( IOException | NullPointerException e ) {
/* 一个块来处理2种异常 */
}
6.1.4. Power assertion
Groovy与Java共享 assert
关键字,但在Groovy中表现得非常不同。首先,始终执行Groovy中的断言,而与JVM的 -ea
标志无关。它使它成为单元测试的首选。“power asserts”的概念与Groovy assert
的行为方式直接相关。
power assertion分解为3个部分:
assert [left expression] == [right expression] : (optional message)
断言的结果与Java中的结果非常不同。如果断言为真,则没有任何反应。如果断言为假,则它提供被断言的表达式的每个子表达式的值的直观表示。例如:
assert 1+1 == 3
将产生:
Caught: Assertion failed:
assert 1+1 == 3
| |
2 false
当表达式更复杂时,幂断言变得非常有趣,如下一个示例所示:
def x = 2
def y = 7
def z = 5
def calc = { a,b -> a*b+1 }
assert calc(x,y) == [x,z].sum()
这将打印每个子表达式的值:
assert calc(x,y) == [x,z].sum()
| | | | | | |
15 2 7 | 2 5 7
false
如果你不想像上面那样打印漂亮的错误消息,可以通过更改断言的可选消息部分来回退到自定义错误消息,如下例所示:
def x = 2
def y = 7
def z = 5
def calc = { a,b -> a*b+1 }
assert calc(x,y) == z*z : 'Incorrect computation result'
这将打印以下错误消息:
Incorrect computation result. Expression: (calc.call(x, y) == (z * z)). Values: z = 5, z = 5
6.1.5. 标签语句
任何语句都可以与标签相关联。标签不会影响代码的语义,可以用来使代码更容易阅读,如下例所示:
given:
def x = 1
def y = 2
when:
def z = x+y
then:
assert z == 3
尽管没有更改标记语句的语义,但可以使用break指令中的标签作为跳转目标,如下一个示例所示。但是,即使这是允许的,这种编码风格通常被认为是一种不好的做法:
for (int i=0;i<10;i++) {
for (int j=0;j<i;j++) {
println "j=$j"
if (j == 5) {
break exit
}
}
exit: println "i=$i"
}
重要的是要理解默认情况下标签对代码的语义没有影响,但它们属于抽象语法树(AST),因此AST转换可以使用该信息对代码执行转换,因此导致不同的语义。这正是 Spock Framework为简化测试所做的工作。
6.2. 表达式
(TBD)
6.2.1. GPath表达式
GPath是一种集成到Groovy中的路径表达式语言,它允许识别嵌套结构化数据的一部分。从这个意义上说,它具有与XPath对XML相似的目标和范围。GPath通常用于处理XML的上下文中,但它确实适用于任何对象图。XPath使用类似文件系统的路径表示法,树形层次结构的部分用斜杠 /
分隔,GPath使用点对象表示法来执行对象导航。
例如,你可以指定感兴趣的对象或元素的路径:
-
a.b.c
→ 对于XML,产生a
内的b
内的所有c
元素 -
a.b.c
→ 对于POJO,产生a
的所有b
属性下的c
属性(类似于JavaBeans中的a.getB().getC()
)
在这两种情况下,GPath表达式都可以被视为对象图上的查询。对于POJO,对象图通常由通过对象实例化和组合编写的程序构建;对于XML处理,对象图是解析XML文本的结果,通常使用类似XmlParser或XmlSlurper的类。有关在Groovy中使用XML的更深入的详细信息,请参阅处理XML。
查询从XmlParser或XmlSlurper生成的对象图时,GPath表达式可以引用使用 @
表示法在元素上定义的属性:
-
a["@href"]
→ map-like表示法:a元素的所有href属性 -
a.'@href'
→ property表示法:表达相同含义的另一种方式 -
a.@href
→ direct表示法:表达相同含义的另一种方式
对象导航
让我们看一个简单对象图上的GPath表达式示例,使用java反射获得。假设你处于具有另一个名为 aMethodFoo
的方法的类的非静态方法中
void aMethodFoo() { println "This is aMethodFoo." } (0)
以下GPath表达式将获取该方法的名称:
assert ['aMethodFoo'] == this.class.methods.name.grep(~/.*Foo/)
更确切地说,上面的GPath表达式生成一个String列表,每个String都是 this
中以 Foo
结尾的现有方法的名称。
现在,给定了该类中定义的以下方法:
void aMethodBar() { println "This is aMethodBar." } (1)
void anotherFooMethod() { println "This is anotherFooMethod." } (2)
void aSecondMethodBar() { println "This is aSecondMethodBar." } (3)
那么下面的GPath表达式将获得(1)和(3)的名称,但不是(2)或(0):
assert ['aMethodBar', 'aSecondMethodBar'] as Set == this.class.methods.name.grep(~/.*Bar/) as Set
表达式解构
我们可以分解表达式 this.class.methods.name.grep(~/.*Bar/)
以了解如何评估GPath:
this.class
-
属性访问器,相当于Java中的
this.getClass()
,产生一个Class
对象。 this.class.methods
-
属性访问器,等效于
this.getClass().getMethods()
,产生一个Method
对象数组。 this.class.methods.name
-
在数组的每个元素上应用属性访问器并生成结果列表。
this.class.methods.name.grep(…)
-
对
this.class.methods.name
生成的列表的每个元素调用grep
方法,并生成结果列表。
像 this.class.methods
这样的子表达式产生一个数组,因为这是调用Java中的 this.getClass().getMethods()
产生的。GPath
表达式没有约定,其中 s
表示列表或类似的东西。
GPath表达式的一个强大功能是对集合的属性访问转换为集合的每个元素的属性访问,并将结果收集到集合中。因此,表达式 this.class.methods.name
在Java中表示如下:
List<String> methodNames = new ArrayList<String>();
for (Method method : this.getClass().getMethods()) {
methodNames.add(method.getName());
}
return methodNames;
数组访问表示法也可用于存在集合的GPath表达式中:
assert 'aSecondMethodBar' == this.class.methods.name.grep(~/.*Bar/).sort()[1]
数组访问在GPath表达式中从零开始
用于XML导航的GPath
以下是XML文档和各种形式的GPath表达式的示例:
def xmlText = """
| <root>
| <level>
| <sublevel id='1'>
| <keyVal>
| <key>mykey</key>
| <value>value 123</value>
| </keyVal>
| </sublevel>
| <sublevel id='2'>
| <keyVal>
| <key>anotherKey</key>
| <value>42</value>
| </keyVal>
| <keyVal>
| <key>mykey</key>
| <value>fizzbuzz</value>
| </keyVal>
| </sublevel>
| </level>
| </root>
"""
def root = new XmlSlurper().parseText(xmlText.stripMargin())
assert root.level.size() == 1 (1)
assert root.level.sublevel.size() == 2 (2)
assert root.level.sublevel.findAll { it.@id == 1 }.size() == 1 (3)
assert root.level.sublevel[1].keyVal[0].key.text() == 'anotherKey' (4)
1 | root 目录下有一个 level 节点 |
2 | root/level 下有两个 sublevel 节点 |
3 | 有一个 sublevel 元素具有值为 1 的 id 属性 |
4 | root/level 下第二个 sublevel 元素的第一个 keyVal 元素的 key 元素的文本值是 'anotherKey' |
6.3. 提升和强转
6.3.1. 数字提升
数字提升的规则在数学运算部分中已介绍。
6.3.2. 闭包到类型的强转
为SAM类型赋值闭包
SAM类型是定义单个抽象方法的类型。这包括:
函数式接口
interface Predicate<T> {
boolean accept(T obj)
}
单抽象方法的抽象类
abstract class Greeter {
abstract String getName()
void greet() {
println "Hello, $name"
}
}
可以使用 as
运算符将任何闭包转换为SAM类型:
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
但是,as Type
表达式是Groovy 2.2.0以来的可选项。你可以省略它并简写为:
Predicate filter = { it.contains 'G' }
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' }
greeter.greet()
这意味着你还可以使用方法指针,如以下示例所示:
boolean doFilter(String s) { s.contains('G') }
Predicate filter = this.&doFilter
assert filter.accept('Groovy') == true
Greeter greeter = GroovySystem.&getVersion
greeter.greet()
调用接受带有闭包的SAM类型的方法
带有闭包SAM类型强转的第二个也许更重要的用例是调用接受SAM类型的方法。想象一下以下方法:
public <T> List<T> filter(List<T> source, Predicate<T> predicate) {
source.findAll { predicate.accept(it) }
}
然后,你可以使用闭包调用它,而无需创建接口的显式实现:
assert filter(['Java','Groovy'], { it.contains 'G'} as Predicate) == ['Groovy']
但是从Groovy 2.2.0开始,你也可以省略显式强转并调用方法,就好像它使用了一个闭包:
但是从Groovy 2.2.0开始,你也可以省略显式强制并调用方法,就好像它使用了一个闭包:
如你所见,这样做的好处是可以让你使用闭包语法进行方法调用,也就是说将闭包放在括号之外,从而提高代码的可读性。
闭包到任意类型的强转
除了SAM类型之外,还可以将闭包强制转换为任何类型,特别是接口。让我们定义以下接口:
interface FooBar {
int foo()
void bar()
}
你可以使用 as
关键字强制转换闭包为接口:
def impl = { println 'ok'; 123 } as FooBar
这将生成一个类,所有方法都使用闭包来实现:
assert impl.foo() == 123
impl.bar()
但也有可能强制转换闭包为任意类。例如,我们可以在不更改断言的情况下使用 class
替换我们定义的 interface
:
class FooBar {
int foo() { 1 }
void bar() { println 'bar' }
}
def impl = { println 'ok'; 123 } as FooBar
assert impl.foo() == 123
impl.bar()
6.3.3. Map到类型的强转
通常使用单个闭包来实现接口或具有多个方法的类是不可取的。作为替代方案,Groovy允许你将Map强制转换为接口或类。在这种情况下,Map的键被解释为方法名称,而值是方法实现。以下示例说明了将映射强制转换为 Iterator
:
def map
map = [
i: 10,
hasNext: { map.i > 0 },
next: { map.i-- },
]
def iter = map as Iterator
当然这是一个相当人为的例子,但说明了这个概念。你只需要实现那些实际调用的方法,但是如果调用了一个在map中不存在的方法,则会抛出 MissingMethodException
或 UnsupportedOperationException
,具体取决于传递给调用的参数,如下例所示:
interface X {
void f()
void g(int n)
void h(String s, int n)
}
x = [ f: {println "f called"} ] as X
x.f() // 方法存在
x.g() // 出现 MissingMethodException
x.g(5) // 出现 UnsupportedOperationException
异常的类型取决于调用本身:
-
MissingMethodException
如果调用的参数与接口/类的参数不匹配 -
UnsupportedOperationException
如果调用的参数匹配接口/类的重载方法之一
6.3.4. 字符串到枚举的强转
Groovy允许透明 String
(或 GString
)枚举值强转。想象一下,你定义以下枚举:
enum State {
up,
down
}
然后你可以为枚举赋值一个字符串而不必使用显式强转:
State st = 'up'
assert st == State.up
也可以使用 GString
作为值:
def val = "up"
State st = "${val}"
assert st == State.up
但是,以下这样会抛出运行时错误(IllegalArgumentException
):
State st = 'not an enum value'
请注意,也可以在switch语句中使用隐式强转:
State switchState(State st) {
switch (st) {
case 'up':
return State.down // 显式常量
case 'down':
return 'up' // 对返回类型的隐式强转
}
}
特别是,看看 case
如何使用字符串常量。但是如果你使用带有 String
参数的枚举来调用方法,你仍然必须使用显式强转:
assert switchState('up' as State) == State.down
assert switchState(State.down) == State.up
6.3.5. 自定义类型强转
类可以通过实现 asType
方法来定义自定义强转策略。使用 as
运算符调用自定义强转,并且从不隐式。例如,假设你定义了两个类,Polar
和 Cartesian
,如下例所示:
class Polar {
double r
double phi
}
class Cartesian {
double x
double y
}
并且你想要从Polar坐标转换为Cartesian坐标。一种方法是在 Polar
类中定义 asType
方法:
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
允许你使用 as
强制运算符:
def sigma = 1E-16
def polar = new Polar(r:1.0,phi:PI/2)
def cartesian = polar as Cartesian
assert abs(cartesian.x-sigma) < sigma
总而言之,Polar
类看起来像这样:
class Polar {
double r
double phi
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
}
但是也可以在 Polar
类之外定义 asType
,如果你想为你没有源代码的类或“封闭”类定义自定义强转策略,这可能是实用的,例如使用metaClass:
Polar.metaClass.asType = { Class target ->
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
6.3.6. 类字面量vs变量和as运算符
只有在对类有静态引用时才可以使用 as
关键字,如下面的代码所示:
interface Greeter {
void greet()
}
def greeter = { println 'Hello, Groovy!' } as Greeter // Greeter是其类的静态引用
greeter.greet()
但是如果你通过反射获得类,例如通过调用 Class.forName
会怎样?
Class clazz = Class.forName('Greeter')
尝试使用带有 as
关键字的类的引用将失败:
greeter = { println 'Hello, Groovy!' } as clazz
// throws:
// unable to resolve class clazz
// @ line 9, column 40.
// greeter = { println 'Hello, Groovy!' } as clazz
它失败了因为 as
关键字只适用于类字面量。相反,你需要调用 asType
方法:
greeter = { println 'Hello, Groovy!' }.asType(clazz)
greeter.greet()
6.4. 可选性
6.4.1. 可选括号
如果至少有一个参数并且没有歧义,方法调用可以省略括号:
println 'Hello World'
def maximum = Math.max 5, 10
没有参数或模糊方法调用的方法调用需要括号:
println()
println(Math.max(5, 10))
6.4.2. 可选分号
在Groovy行的末尾分号可以省略,如果该行只包含一个语句。这意味着:
assert true;
可以更方便地写为:
assert true
一行中的多个语句需要用分号分隔它们:
boolean a = true; assert a
6.4.3. 可选的return关键字
在Groovy中,返回在方法或闭包的主体中计算的最后一个表达式。这意味着 return
关键字是可选的。
int add(int a, int b) {
return a+b
}
assert add(1, 2) == 3
可简化为:
int add(int a, int b) {
a+b
}
assert add(1, 2) == 3
6.4.4. 可选的public关键字
默认情况下,Groovy类和方法是 public
的。因此这个类:
public class Server {
public String toString() { "a server" }
}
与这个类相同:
class Server {
String toString() { "a server" }
}
6.5. Groovy真值
Groovy通过应用下面给出的规则来决定表达式是真还是假。
6.5.1. 布尔表达式
如果相应的布尔值为 true
,则为True。
assert true
assert !false
6.5.2. 集合和数组
非空集合和数组是真。
assert [1, 2, 3]
assert ![]
6.5.3. 匹配器
如果匹配器至少有一个匹配,则为真。
assert ('a' =~ /a/)
assert !('a' =~ /b/)
6.5.4. 迭代器和枚举
具有更多元素的迭代器和枚举被强转为真。
assert [0].iterator()
assert ![].iterator()
Vector v = [0] as Vector
Enumeration enumeration = v.elements()
assert enumeration
enumeration.nextElement()
assert !enumeration
6.5.5. 映射
非空Maps评估为true。
assert ['one' : 1]
assert ![:]
6.5.6. 字符串
非空字符串,GStrings和CharSequences被强转为true。
assert 'a'
assert !''
def nonEmpty = 'a'
assert "$nonEmpty"
def empty = ''
assert !"$empty"
6.5.7. 数字
非零数字是真。
assert 1
assert 3.5
assert !0
6.5.8. 对象引用
非空对象引用被强转为true。
assert new Object()
assert !null
6.5.9. 使用 asBoolean()
方法自定义真值
为了自定义groovy是否将对象计算为 true
或 false
,请实现 asBoolean()
方法:
class Color {
String name
boolean asBoolean(){
name == 'green' ? true : false
}
}
Groovy将调用此方法将你的对象强转为布尔值,例如:
assert new Color(name: 'green')
assert !new Color(name: 'red')
6.6. 类型
6.6.1. 可选类型
可选类型是即使你没有在变量上放置显式类型,程序也可以正常工作。作为一种动态语言,Groovy自然地实现了该功能,例如当你声明一个变量时:
String aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | foo 使用显式类型 String 声明 |
2 | 我们可以在 String 上调用 toUpperCase 方法 |
Groovy让你可以这样写:
def aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | 使用 def 声明 foo |
2 | 我们仍然可以调用 toUpperCase 方法,因为在运行时被解析为 String 类型 |
所以你在这里使用显式类型并不重要。将此功能与静态类型检查结合使用时尤其有趣,因为类型检查器会执行类型推断。
同样,Groovy没有强制要求在方法中声明参数的类型:
String concat(String a, String b) {
a+b
}
assert concat('foo','bar') == 'foobar'
可以使用 def
作为返回类型和参数类型重写,以便利用鸭子类型,如下例所示:
def concat(def a, def b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | 返回类型和参数类型都使用 def |
2 | 它让用 String 调用函数成为可能 |
3 | 因为定义了 plus 方法,所以也可以使用 int |
建议在这里使用 def
关键字来描述一个应该适用于任何类型的方法的意图,但从技术上讲,我们可以使用 Object
而结果也是相同的:def
是在Groovy中,严格等同于使用 Object
。
最终,可以从返回类型和参数描述符中完全删除类型。但是如果要从返回类型中删除它,则需要为该方法添加一个显式修饰符,以便编译器从方法声明和方法调用之间的差异中分辨它们,如下例所示:
private concat(a,b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | 如果我们想省略返回类型,则必须显式设置修饰符。 |
2 | 仍然可以使用 String 调用方法 |
3 | 还有 int |
省略公共API的方法参数或方法返回值类型通常被认为是不良做法。虽然在局部变量中使用 def
并不是真正的问题,因为变量的可见性仅限于方法本身,而在方法参数上设置时,def
将在方法签名中转换为 Object
,这使得用户很难知道这是参数的预期类型。这意味着你应该将此限制为明确依赖于鸭子类型的情况。
6.6.2. 静态类型检查
默认情况下,Groovy在编译时执行最少的类型检查。由于它主要是一种动态语言,因此在编译时无法检查静态编译器通常会执行的操作。通过运行时元编程添加的方法可能会改变类或对象的运行时行为。让我们在下面的例子中说明原因:
class Person { (1)
String firstName
String lastName
}
def p = new Person(firstName: 'Raymond', lastName: 'Devos') (2)
assert p.formattedName == 'Raymond Devos' (3)
1 | Person 类只定义了两个属性 firstName 和 lastName |
2 | 我们可以创建一个Person实例 |
3 | 并调用名为 formattedName 的方法 |
在动态语言中,如上例所示的代码不会抛出任何错误。怎么会这样?在Java中,这通常会在编译时失败。但是,在Groovy中,它不会在编译时失败,如果编码正确,也不会在运行时失败。实际上,为了使其在运行时工作,一种可能性是依赖于运行时元编程。所以只需在 Person
类声明后添加这一行就足够了:
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }
这意味着一般来说,在Groovy中,你不能对超出声明类型的对象类型做出任何假设,即使你知道它,你也无法在编译时确定将调用哪个方法,或者哪个属性将被检索。从编写DSL到测试,它很有趣,本手册的其他部分对此进行了讨论。
但是,如果你的程序不依赖于动态功能并且你来自静态世界(特别是来自Java思维模式),那么在编译时不能捕获这样的“错误”可能会令人惊讶。正如我们在前面的例子中看到的那样,编译器无法确定这是一个错误。为了使其知道它,你必须明确地指示编译器你正在切换到类型检查模式。这可以通过使用 @groovy.lang.TypeChecked
注解类或方法来完成。
激活类型检查时,编译器执行更多工作:
-
类型推断已激活,这意味着即使你对局部变量使用
def
,类型检查器也能够从赋值中推断出变量的类型 -
方法调用在编译时解析,这意味着如果没有在类上声明方法,编译器将抛出错误
-
通常,你将用于在静态语言中查找的所有编译时错误将会出现:找不到方法,找不到属性,方法调用的类型不兼容,数字精度错误,…
在本节中,我们将描述类型检查器在各种情况下的行为,并解释在代码上使用 @TypeChecked
的限制。
@TypeChecked
注解
在编译时激活类型检查
groovy.lang.TypeChecked
注解启用了类型检查。它可以放在一个类上:
@groovy.transform.TypeChecked
class Calculator {
int sum(int x, int y) { x+y }
}
或一个方法上:
lass Calculator {
@groovy.transform.TypeChecked
int sum(int x, int y) { x+y }
}
在第一种情况下,将对带注解的类的所有方法,属性,字段,内部类……进行类型检查,而在第二种情况下,仅对标注类型检查的方法和可能的闭包或匿名内部类进行类型检查。
跳过部分
可以限制类型检查的范围。例如,如果类被选中,则可以通过使用 @TypeChecked(TypeCheckingMode.SKIP)
来指示类型检查器跳过方法:
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
@TypeChecked (1)
class GreetingService {
String greeting() { (2)
doGreet()
}
@TypeChecked(TypeCheckingMode.SKIP) (3)
private String doGreet() {
def b = new SentenceBuilder()
b.Hello.my.name.is.John (4)
b
}
}
def s = new GreetingService()
assert s.greeting() == 'Hello my name is John'
1 | GreetingService 类标记为需类型检查 |
2 | 所以 greeting 方法会自动进行类型检查 |
3 | 但是 doGreet 标有 SKIP |
4 | 类型检查器不会在这里抱怨缺少属性 |
在前面的示例中,SentenceBuilder
依赖于动态代码。没有真正的 Hello
方法或属性,因此类型检查器通常会抱怨并且编译会失败。由于使用构建器的方法使用 TypeCheckingMode.SKIP
标记,因此对于此方法将跳过类型检查,因此即使类的其余部分已选中,代码也将进行编译通过。
以下部分描述了Groovy中类型检查的语义。
类型检查赋值
当且仅当以下情况时,可以将类型 A
的对象 o
赋值给类型为 T
的变量:
-
T
等于A
Date now = new Date()
-
或
T
是String
,boolean
,Boolean
或Class
之一
String s = new Date() // 隐式调用toString
Boolean boxed = 'some string' // Groovy真值
boolean prim = 'some string' // Groovy真值
Class clazz = 'java.lang.String' // class强转
-
或
o
为null
,T
不是基本类型
String s = null // passes
int i = null // fails
-
或
T
是数组,A
是数组,A
的组件类型可分配给T
的组件类型
int[] i = new int[4] // passes
int[] i = new String[4] // fails
-
或
T
是数组,A
是列表,A
的组件类型可分配给T
的组件类型
int[] i = [1,2,3] // passes
int[] i = [1,2, new Date()] // fails
-
或
T
是A
的超类
AbstractList list = new ArrayList() // passes
LinkedList list = new ArrayList() // fails
-
或
T
是由A
实现的接口
List list = new ArrayList() // passes
RandomAccess list = new LinkedList() // fails
-
或者
T
或A
是基本类型,它们的装箱类型是可赋值的
int i = 0
Integer bi = 1
int x = new Integer(123)
double d = new Float(5f)
-
或
T
继承groovy.lang.Closure
且A
是SAM类型(单一抽象方法类型)
Runnable r = { println 'Hello' }
interface SAMType {
int doSomething()
}
SAMType sam = { 123 }
assert sam.doSomething() == 123
abstract class AbstractSAM {
int calc() { 2* value() }
abstract int value()
}
AbstractSAM c = { 123 }
assert c.calc() == 246
-
或
T
和A
派生自java.lang.Number
并符合下表
T | A | 样例 |
---|---|---|
Double |
除了BigDecimal或BigInteger之外的任何数字 |
Table of Contents
|
Float |
除BigDecimal,BigInteger或Double之外的任何类型 |
Table of Contents
|
Long |
除BigDecimal,BigInteger,Double或Float之外的任何类型 |
Table of Contents
|
Integer |
除BigDecimal,BigInteger,Double,Float或Long之外的任何类型 |
Table of Contents
|
Short |
除BigDecimal,BigInteger,Double,Float,Long或Integer之外的任何类型 |
Table of Contents
|
Byte |
Byte |
Table of Contents
|
列表和映射构造函数
除了上面的赋值规则之外,如果赋值被视为无效,则在类型检查模式下,如果符合以下情况,则可以将列表或映射字面量 A
赋值给类型为 T
的变量:
-
赋值是一个变量声明,
A
是一个列表,T
有一个构造函数,其参数与列表中元素的类型相匹配 -
赋值是一个变量声明,
A
是一个映射,T
有一个no-arg构造函数和每个映射键的属性
例如:
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person classic = new Person('Ada','Lovelace')
你可以使用“列表构造函数”:
Person list = ['Ada','Lovelace']
或“映射构造函数”:
Person map = [firstName:'Ada', lastName:'Lovelace']
如果使用映射构造函数,则会对映射的键执行其他检查,以检查是否定义了同名属性。例如,以下内容将在编译时失败:
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person map = [firstName:'Ada', lastName:'Lovelace', age: 24] (1)
1 | 类型检查器将在编译时抛出错误 No such property: age for class: Person |
方法解析
在类型检查模式下,方法在编译时解析。解析按方法名称和参数工作,与方法返回值类型无关。参数类型与遵循这些规则的参数类型相匹配:
当且仅当以下情况时,类型 A
的参数 o
可用于类型 T
的参数:
-
T
等于A
int sum(int x, int y) {
x+y
}
assert sum(3,4) == 7
-
或
T
是一个字符串,A
是一个GString
String format(String str) {
"Result: $str"
}
assert format("${3+4}") == "Result: 7"
-
或
o
为null
,T
不是基本类型
String format(int value) {
"Result: $value"
}
assert format(7) == "Result: 7"
format(null) // fails
-
或
T
是数组,A
是数组,A
的组件类型可分配给T
的组件类型
String format(String[] values) {
"Result: ${values.join(' ')}"
}
assert format(['a','b'] as String[]) == "Result: a b"
format([1,2] as int[]) // fails
-
或
T
是A
的超类
String format(AbstractList list) {
list.join(',')
}
format(new ArrayList()) // passes
String format(LinkedList list) {
list.join(',')
}
format(new ArrayList()) // fails
-
或
T
是由A
实现的接口
String format(List list) {
list.join(',')
}
format(new ArrayList()) // passes
String format(RandomAccess list) {
'foo'
}
format(new LinkedList()) // fails
-
或者
T
或A
是基本类型,它们的装箱类型是可赋值的
int sum(int x, Integer y) {
x+y
}
assert sum(3, new Integer(4)) == 7
assert sum(new Integer(3), 4) == 7
assert sum(new Integer(3), new Integer(4)) == 7
assert sum(new Integer(3), 4) == 7
-
或
T
继承groovy.lang.Closure
且A
是SAM类型(单一抽象方法类型)
interface SAMType {
int doSomething()
}
int twice(SAMType sam) { 2*sam.doSomething() }
assert twice { 123 } == 246
abstract class AbstractSAM {
int calc() { 2* value() }
abstract int value()
}
int eightTimes(AbstractSAM sam) { 4*sam.calc() }
assert eightTimes { 123 } == 984
-
或者
T
和A
派生自java.lang.Number
并且符合与数字赋值相同的规则
如果在编译时未找到具有适当名称和参数的方法,则会引发错误。以下示例说明了与“普通”Groovy的区别:
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine 是一个错误,但由于我们处于动态模式,因此在编译时不会捕获错误 |
上面的示例显示了Groovy能够编译的类。但是,如果你尝试创建 MyService
实例并调用 doSomething
方法,那么它将在运行时失败,因为 printLine
不存在。当然,我们已经展示了Groovy如何使它成为一个完全有效的调用,例如通过捕获 MethodMissingException
或实现自定义元类,但如果这种情况并不是你想要的,那么 @TypeChecked
也许会派上用场:
@groovy.transform.TypeChecked
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine 出现编译时错误 |
只需添加 @TypeChecked
将触发编译时方法解析。类型检查器将尝试在 MyService
类上找到接受 String
参数的方法 printLine
,但找不到。编译失败时会显示以下消息:
Cannot find matching method MyService#printLine(java.lang.String)
理解类型检查器背后的逻辑非常重要:它是一个编译时检查,因此根据定义,类型检查器不知道你执行的任何类型的运行时元编程。这意味着如果激活类型检查,那么在没有 @TypeChecked
的情况下完全有效的代码也将不再编译。如果你想到鸭子类型,尤其如此:
class Duck {
void quack() { (1)
println 'Quack!'
}
}
class QuackingBird {
void quack() { (2)
println 'Quack!'
}
}
@groovy.transform.TypeChecked
void accept(quacker) {
quacker.quack() (3)
}
accept(new Duck()) (4)
1 | 我们定义了一个 Duck 类,它定义了一个 quack 方法 |
2 | 我们定义另一个 QuackingBird 类,它也定义了一个 quack 方法 |
3 | quacker 是松散类型的,所以由于该方法是 @TypeChecked ,我们将获得编译时错误 |
4 | 即使在非类型检查的Groovy中,这可以通过 |
有一些可能的解决方法,比如引入一个接口,但基本上,通过激活类型检查,你可以获得类型安全性但是你放弃了该语言的某些功能。希望Groovy引入一些功能,如流类型,以减少类型检查和非类型检查Groovy之间的差距。
类型推断
原则
使用 @TypeChecked
注解代码时,编译器会执行类型推断。它不仅仅依赖于静态类型,而且还使用各种技术来推断变量的类型,返回类型,字面量……,这样即使激活类型检查器,代码仍然保持尽可能干净。
最简单的例子是推断变量的类型:
def message = 'Welcome to Groovy!' (1)
println message.toUpperCase() (2)
println message.upper() // compile time error (3)
1 | 使用 def 关键字声明变量 |
2 | 类型检查器允许调用 toUpperCase |
3 | 调用 upper 将在编译时失败 |
可调用 toUpperCase
的原因是因为 message
的类型被推断为 String
。
变量vs类型推断中的字段
值得注意的是,虽然编译器对局部变量执行类型推断,但它不对字段执行任何类型的推断,总是回退到声明的字段类型。为了说明这一点,我们来看看这个例子:
class SomeClass {
def someUntypedField (1)
String someTypedField (2)
void someMethod() {
someUntypedField = '123' (3)
someUntypedField = someUntypedField.toUpperCase() // compile-time error (4)
}
void someSafeMethod() {
someTypedField = '123' (5)
someTypedField = someTypedField.toUpperCase() (6)
}
void someMethodUsingLocalVariable() {
def localVariable = '123' (7)
someUntypedField = localVariable.toUpperCase() (8)
}
}
1 | someUntypedField 使用 def 作为声明类型 |
2 | someTypedField 使用String作为声明类型 |
3 | 我们可以为 someUntypedField 赋值任何东西 |
4 | 但是在编译时调用 toUpperCase 失败,因为该字段没有适当的类型 |
5 | 我们可以将 String 分配给 String 类型的字段 |
6 | 并且这次允许使用 toUpperCase |
7 | 如果我们将一个 String 分配给一个局部变量 |
8 | 然后在局部变量上可调用 toUpperCase |
为何如此不同?原因是线程安全。在编译时,我们无法保证字段的类型。任何线程都可以在任何时候访问任何字段,在方法中为某个字段指定了某种类型的变量,并且在之后的行中使用时,另一个线程可能已经更改了字段的内容。局部变量不是这种情况:我们知道它们是否“逃逸”,因此我们可以确保变量的类型随着时间的推移是恒定的(或不是)。请注意,即使字段是final字段,JVM也不保证它,因此无论字段是否是final,类型检查器的行为都是一样的。
这是我们建议使用有类型字段的原因之一。由于类型推断,将def用于局部变量是完全正确的,对于字段来说情况并非如此,这些字段也会用作类的公共API,因此类型很重要。
集合类型推断
Groovy为各种类型的字面量提供了语法。Groovy中有三种本地集合字面量:
-
列表,使用
[]
字面量 -
映射,使用
[:]
字面量 -
范围,使用
from..to
(包含)和from..<to
(排除)字面量
字面量的推断类型取决于字面量内的元素,如下表所示:
字面量 |
推断类型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如你所见,除了 IntRange
之外,推断类型使用泛型类型来描述集合的内容。如果集合包含不同类型的元素,则类型检查器仍会执行组件的类型推断,但使用最小上限的概念。
最小上限
在Groovy中,A
和 B
两种类型的最小上限被定义为以下类型:
-
超类对应于
A
和B
的公共超类 -
接口对应于
A
和B
实现的接口 -
如果
A
或B
是基本类型且A
不等于B
,则A
和B
的最小上限是其装箱类型的最小上限
如果 A
和 B
只有一个接口,并且它们的公共超类是 Object
,那么两者的LUB就是公共接口。
最小上限表示可以分配给 A
和 B
的最小类型。例如,如果 A
和 B
都是 String
,那么它们的LUB(least upper bound)也是 String
。
class Top {}
class Bottom1 extends Top {}
class Bottom2 extends Top {}
assert leastUpperBound(String, String) == String (1)
assert leastUpperBound(ArrayList, LinkedList) == AbstractList (2)
assert leastUpperBound(ArrayList, List) == List (3)
assert leastUpperBound(List, List) == List (4)
assert leastUpperBound(Bottom1, Bottom2) == Top (5)
assert leastUpperBound(List, Serializable) == Object (6)
1 | String 和 String 的LUB是 String |
2 | ArrayList 和 LinkedList 的LUB是它们公共的超类型 AbstractList |
3 | ArrayList 和 List 的LUB是它们的公共接口 List |
4 | 两个相同接口的LUB是该接口本身 |
5 | Bottom1 和 Bottom2 的LUB是它们的超类 Top |
6 | 两种没有任何公共类型的LUB是 Object |
在这些示例中,LUB始终表示为普通的JVM支持类型。但是Groovy在内部将LUB表示为一种可能更复杂的类型,并且你将无法使用它来定义变量。为了说明这一点,让我们继续这个例子:
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}
Bottom
和 SerializableFooImpl
的最小上限是什么?它们没有共同的超类(除了 Object
),但它们共享2个接口(Serializable
和 Foo
),因此它们的最小上限是表示两个接口(Serializable
和 Foo
)的并集的类型。此类型无法在源代码中定义,但Groovy知道它。
在集合类型推断(以及一般的泛型类型推断)的上下文中,这变得很方便,因为组件的类型被推断为最小上限。我们可以在以下示例中说明为什么这很重要:
interface Greeter { void greet() } (1)
interface Salute { void salute() } (2)
class A implements Greeter, Salute { (3)
void greet() { println "Hello, I'm A!" }
void salute() { println "Bye from A!" }
}
class B implements Greeter, Salute { (4)
void greet() { println "Hello, I'm B!" }
void salute() { println "Bye from B!" }
void exit() { println 'No way!' } (5)
}
def list = [new A(), new B()] (6)
list.each {
it.greet() (7)
it.salute() (8)
it.exit() (9)
}
1 | Greeter 接口定义了一个方法 greet |
2 | Salute 接口定义了一个方法 salute |
3 | A 类实现了 Greeter 和 Salute ,但没有显式的接口继承两者 |
4 | B 同样 |
5 | 但 B 定义了另一个 exit 方法 |
6 | list 的类型被推断为“A 和 B 的LUB列表” |
7 | 因此可以通过 Greeter 接口调用在 A 和 B 上定义的 greet |
8 | 也可以通过 Salute 接口调用在 A 和 B 上定义的 salute |
9 | 但是调用 exit 会出现编译时错误,因为它不属于 A 和 B 的LUB(仅在 B 中定义) |
错误消息如下所示:
[Static type checking] - Cannot find matching method Greeter or Salute#exit()
表示 exit
方法既没有在 Greeter
也没有 Salute
上定义,它们是在 A
和 B
的最小上限中定义的两个接口。
instanceof 推断
在正常的非类型检查Groovy中,你可以编写如下内容:
class Greeter {
String greeting() { 'Hello' }
}
void doSomething(def o) {
if (o instanceof Greeter) { (1)
println o.greeting() (2)
}
}
doSomething(new Greeter())
1 | 使用 instanceof 检查保护方法调用 |
2 | 调用方法 |
方法调用因动态调度而起作用(该方法在运行时选择)。Java中的等效代码需要在调用 greeting
方法之前手动将 o
转换为 Greeter
,因为会在编译时选择方法:
if (o instanceof Greeter) {
System.out.println(((Greeter)o).greeting());
}
但是,在Groovy中,即使在 doSomething
方法上添加 @TypeChecked
(从而激活类型检查),也不需要强制转换。编译器嵌入了instanceof推断使得强制转换可选。
流类型
流类型是Groovy在类型检查模式中的一个重要概念,也是类型推断的扩展。我们的想法是编译器能够在代码流中推断出变量的类型,而不仅仅是在初始化时:
@groovy.transform.TypeChecked
void flowTyping() {
def o = 'foo' (1)
o = o.toUpperCase() (2)
o = 9d (3)
o = Math.sqrt(o) (4)
}
1 | 首先,o 使用 def 声明并赋值一个 String |
2 | 编译器推断 o 是一个 String ,因此允许调用 toUpperCase |
3 | o 被重新赋值为 double |
4 | 调用 Math.sqrt 会通过编译,因为编译器知道此时 o 是 double |
因此类型检查器能意识到变量的具体类型随时间变化的事实。特别是,如果你将最后一个赋值替换为:
o = 9d
o = o.toUpperCase()
类型检查器现在将在编译时失败,因为它知道在调用 toUpperCase
时 o
是 double
,因此它是类型错误。
重要的是要理解,使用 def
声明一个触发类型推断的变量并不是事实不变的。流类型适用于任何类型的任何变量。声明具有显式类型的变量仅限制可以赋值给变量的内容:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list = list*.toUpperCase() (2)
list = 'foo' (3)
}
1 | list 被声明为未经检查的 List ,并赋值了一个组件类型为 String 的列表字面量 |
2 | 由于流类型的原因,这一行通过了编译:类型检查器知道列表此时是 List<String> |
3 | 但是你无法将 String 赋值给 List ,因此这是类型检查错误 |
你还可以注意到,即使声明变量没有泛型信息,类型检查器也知道组件类型是什么。因此,这样的代码将无法编译:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list.add(1) (2)
}
1 | list 被推断为 List<String> |
2 | 因此,将一个 int 添加到 List<String> 是产生编译时错误 |
修复此问题需要在声明中添加显式泛型类型:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List<? extends Serializable> list = [] (1)
list.addAll(['a','b','c']) (2)
list.add(1) (3)
}
1 | 列表声明为 List<? extends Serializable> 并使用空列表初始化 |
2 | 添加到列表中的元素符合列表的声明类型 |
3 | 所以允许在 List<? extends Serializable> 中添加一个 int 值 |
引入了流类型来减少经典和静态Groovy之间的语义差异。特别要考虑这段代码在Java中的行为:
public Integer compute(String str) {
return str.length();
}
public String compute(Object o) {
return "Nope";
}
// ...
Object string = "Some string"; (1)
Object result = compute(string); (2)
System.out.println(result); (3)
1 | o 被声明为 Object 并赋值了一个 String |
2 | 我们用 o 调用 compute 方法 |
3 | 并打印结果 |
在Java中,此代码将输出 Nope
,因为方法选择是在编译时根据声明的类型完成的。因此,即使 o
在运行时是一个 String
,它仍然是调用的 Object
版本,因为 o
已被声明为 Object
。简而言之,在Java中,声明的类型是最重要的,无论是变量类型,参数类型还是返回类型。
在Groovy中,我们可以写:
int compute(String string) { string.length() }
String compute(Object o) { "Nope" }
Object o = 'string'
def result = compute(o)
println result
但这一次,它将返回 6
,因为所选择的方法是在运行时根据实际参数类型选择的。因此,在运行时,o
是一个 String
,因此使用 String
变量的函数。请注意,此行为与类型检查无关,这是Groovy一般工作的方式:动态调度。
在类型检查Groovy中,我们希望确保类型检查器在编译时选择和在运行时将选择的相同方法。一般来说,由于该语言的语义,这是不可能的,但是我们可以通过流类型使事情变得更好。使用流类型,在调用 compute
方法时,o
被推断为 String
,因此选择采用 String
并返回 int
版本的函数。这意味着我们可以将方法的返回类型推断为 int
,而不是 String
。这对于后续调用和类型安全很重要。
因此,在类型检查Groovy中,流类型是一个非常重要的概念,这也意味着如果应用 @TypeChecked
,则根据参数的推断类型而不是声明的类型选择方法。这不能确保100%类型安全,因为类型检查器可能选择了错误的方法,但它确保了与动态Groovy最接近的语义。
高级类型推断
class Top {
void methodFromTop() {}
}
class Bottom extends Top {
void methodFromBottom() {}
}
def o
if (someCondition) {
o = new Top() (1)
} else {
o = new Bottom() (2)
}
o.methodFromTop() (3)
o.methodFromBottom() // compilation error (4)
1 | 如果 someCondition 为 true ,则为 o 赋值 Top |
2 | 如果 someCondition 为 false ,则为 o 赋值 Bottom |
3 | 调用 methodFromTop 是安全的 |
4 | 但是调用 methodFromBottom 不是,所以这是一个编译时错误 |
当类型检查器访问 if/else
控制结构时,它会检查在 if/else
分支中赋值的所有变量,并计算所有赋值的最小上限。此类型是 if/else
块之后的推断变量的类型,因此在此示例中,o
在 if
分支中赋值为 Top
,在 else
分支中赋值为 Bottom
。它们的LUB是 Top
,因此在条件分支之后,编译器将 o
推断为 Top
。因此,将允许调用 methodFromTop
,但不允许调用 methodFromBottom
。
闭包存在相同的推理,特别是闭包共享变量。闭包共享变量是一个变量,它在闭包之外定义,但在闭包内使用,如下例所示:
def text = 'Hello, world!' (1)
def closure = {
println text (2)
}
1 | 声明了一个名为 text 的变量 |
2 | text 用于闭包内部。它是一个闭包共享变量。 |
Groovy允许开发人员使用这些变量而不需要它们是final的。这意味着可以在闭包内重新赋值闭包共享变量:
String result
doSomething { String it ->
result = "Result: $it"
}
result = result?.toUpperCase()
问题是闭包是一个独立的代码块,可以随时执行(或不执行)。特别是,例如,doSomething
可能是异步的。这意味着闭包的主体不属于主控制流。因此,类型检查器还为每个闭包共享变量计算变量的所有赋值的LUB,并将使用该LUB作为闭包范围之外的推断类型,如下例所示:
class Top {
void methodFromTop() {}
}
class Bottom extends Top {
void methodFromBottom() {}
}
def o = new Top() (1)
Thread.start {
o = new Bottom() (2)
}
o.methodFromTop() (3)
o.methodFromBottom() // compilation error (4)
1 | 一个闭包共享变量,首先被赋值为一个 Top |
2 | 在闭包内部,它被赋值为一个 Bottom |
3 | 允许使用 methodFromTop |
4 | methodFromBottom 是一个编译错误 |
在这里当调用 methodFromBottom
时,无法保证在编译时或运行时 o
的类型实际上是一个 Bottom
。它有可能是,但我们无法确定,因为它是异步的。所以类型检查器只允许最小上限的调用,即 Top
。
闭包和类型推断
类型检查器对闭包执行特殊的推断,导致一边进行额外检查,一边提升流畅性。
返回类型推断
类型检查器能够做的第一件事是推断闭包的返回类型。这在以下示例中简单说明:
@groovy.transform.TypeChecked
int testClosureReturnTypeInference(String arg) {
def cl = { "Arg: $arg" } (1)
def val = cl() (2)
val.length() (3)
}
1 | 定义了一个闭包,它返回一个字符串(更准确地说是一个 GString ) |
2 | 我们调用闭包并将结果赋给变量 |
3 | 类型检查器推断闭包将返回一个字符串,因此允许调用 length() |
如你所见,与显式声明其返回类型的方法不同,不需要声明闭包的返回类型:它的类型是从闭包的主体推断出来的。
闭包与方法
值得注意的是,返回类型推断仅适用于闭包。虽然类型检查器可以在方法上执行相同操作,但实际上这是不可取的:通常,可以覆盖方法,并且不可能确保被调用的方法不是被重写的版本。因此,流类型实际上会认为方法返回一些内容,而实际上,它可能会返回其他内容,如下例所示:
@TypeChecked
class A {
def compute() { 'some string' } (1)
def computeFully() {
compute().toUpperCase() (2)
}
}
@TypeChecked
class B extends A {
def compute() { 123 } (3)
}
1 | 类 A 定义了一个 compute 方法,该方法有效地返回一个 String |
2 | 这将导致编译失败,因为 compute 的返回类型是 def (又名 Object ) |
3 | 类 B 继承类 A 并重新定义 compute ,此类型返回 int |
如你所见,如果类型检查器依赖于方法的推断返回类型,使用流类型,类型检查器可以确定能调用 toUpperCase
。它实际上是错误的,因为子类可以重写 compute
并返回不同的对象。这里,B#compute
返回一个 int
,因此在 B
实例上调用 computeFully
的人会看到运行时错误。编译器通过使用方法声明的返回类型而不是推断的返回类型来防止这种情况发生。
为了保持一致性,每种方法的行为都是相同的,即使它们是 static
或 final
。
参数类型推断
除了返回类型之外,闭包还可以从上下文中推断其参数类型。编译器有两种推断参数类型的方法:
-
通过隐式SAM类型强转
-
通过API元数据
为了说明这一点,让我们从一个因为类型检查器无法推断出参数类型,而导致编译失败的示例开始:
class Person {
String name
int age
}
void inviteIf(Person p, Closure<Boolean> predicate) { (1)
if (predicate.call(p)) {
// send invite
// ...
}
}
@groovy.transform.TypeChecked
void failCompilation() {
Person p = new Person(name: 'Gerard', age: 55)
inviteIf(p) { (2)
it.age >= 18 // No such property: age (3)
}
}
1 | inviteIf 方法接受 Person 和 Closure |
2 | 我们使用 Person 和 Closure 调用它 |
3 | 然而,it 并不是静态已知为 Person ,编译失败 |
在这个例子中,闭包体包含 it.age
。对于动态的,而不是类型检查的代码,这将起作用,因为 it
的类型在运行时将是 Person
。不幸的是,在编译时,只需通过读取 inviteIf
的签名无法知道它的类型。
显式闭包参数
简而言之,类型检查器在 inviteIf
方法上没有足够的上下文信息来静态地确定 it
的类型。这意味着方法调用需要像这样重写:
inviteIf(p) { Person it -> (1)
it.age >= 18
}
1 | it 的类型需要明确声明 |
通过明确声明 it
变量的类型,你可以解决此问题并静态检查此代码。
从单抽象方法类型推断出的参数
对于API或框架设计者,有两种方法可以使用户更加优雅,因此他们不必为闭包参数声明显式类型。第一个也是最简单的方法是用SAM类型替换闭包:
interface Predicate<On> { boolean apply(On e) } (1)
void inviteIf(Person p, Predicate<Person> predicate) { (2)
if (predicate.apply(p)) {
// send invite
// ...
}
}
@groovy.transform.TypeChecked
void passesCompilation() {
Person p = new Person(name: 'Gerard', age: 55)
inviteIf(p) { (3)
it.age >= 18 (4)
}
}
1 | 使用 apply 方法声明SAM接口 |
2 | inviteIf 现在使用 Predicate<Person> 而不是 Closure<Boolean> |
3 | 没有必要再显式声明它变量的类型 |
4 | it.age 正确编译,它的类型是从 Predicate#apply 方法签名推断出来的 |
通过使用这种技术,我们利用Groovy中闭包到SAM类型自动强转特性来实现。你是否应该使用SAM类型或闭包的问题实际上取决于你需要做什么。在很多情况下,使用SAM接口就足够了,特别是如果你考虑Java 8中的函数式接口。但是,闭包提供了函数式接口无法访问的功能。特别是,闭包可以有一个委托,所有者可以在被调用之前作为对象(例如,cloned,serialized,curried,…)进行操作。它们还可以支持多个签名(多态)。因此,如果你需要这种操作,最好切换到下面描述的最高级的类型推断注解。
关于闭包参数类型推断需要解决的原始问题,也就是说,静态确定闭包的参数类型而不必显式声明它们是Groovy类型系统继承Java类型系统,不足以描述参数的类型。
@ClosureParams注解
Groovy提供了一个注解 @ClosureParams
,旨在完成类型信息。此注解主要针对希望通过提供类型推断元数据来扩展类型检查器功能的框架和API开发人员。如果你的库使用闭包并且你希望获得最高级别的工具支持,这一点非常重要。
让我们通过修复原始示例,介绍 @ClosureParams
注解来说明这一点:
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam
void inviteIf(Person p, @ClosureParams(FirstParam) Closure<Boolean> predicate) { (1)
if (predicate.call(p)) {
// send invite
// ...
}
}
inviteIf(p) { (2)
it.age >= 18
}
1 | 闭包参数用 @ClosureParams 注解 |
2 | 没有必要为 it 使用显式类型,这是可推断的 |
@ClosureParams
注解最少接受一个参数,该参数被命名为type hint。类型提示是一个类,它负责在编译时为闭包完成类型信息。在此示例中,正在使用的类型提示是 groovy.transform.stc.FirstParam
,它向类型检查器指示闭包将接受一个参数,其类型是方法的第一个参数的类型。在这种情况下,方法的第一个参数是 Person
,因此它向类型检查器指示闭包的第一个参数实际上是 Person
。
第二个可选参数是命名选项。它的语义取决于类型提示类。Groovy附带了各种捆绑类型的提示,如下表所示:
类型提示 | 多态? | 描述和示例 |
---|---|---|
|
No |
Table of Contents
方法的第一个(第二个,第三个)参数类型
|
|
No |
Table of Contents
方法的第一个(第二个,第三个)参数的第一个泛型类型
所有 |
|
No |
Table of Contents
闭包参数类型来自
此类型提示支持单个签名,并且每个参数都使用完全限定的类型名称或基本类型指定为options数组的值。 |
|
Yes |
Table of Contents
闭包的专用类型提示,可以工作在
此类型提示要求第一个参数是 |
|
Yes |
Table of Contents
从某种类型的抽象方法推断闭包参数类型。为每个抽象方法推断签名。
如果上面的示例中有多个签名,则类型检查器只能在每个方法的参数数量不同时推断出参数的类型。在上面的示例中, |
|
Yes |
Table of Contents
从 接受
一个多态闭包,接受一个
一个多态闭包,接受一个
|
即使你使用 FirstParam
,SecondParam
或 ThirdParam
作为类型提示,它也不严格意味着将传递给闭包的参数将是方法调用的第一个(第二个,第三个)参数。它只表示闭包的参数类型与方法调用的第一个(第二个,第三个)参数的类型相同而已。
简而言之,在接受 Closure
的方法上缺少 @ClosureParams
注解不会使编译失败。如果存在(并且它可以存在于Java源代码以及Groovy源代码中),那么类型检查器具有更多信息并且可以执行其他类型推断。这使得此功能对框架开发人员特别有用。
第三个可选参数名为conflictResolutionStrategy。它可以引用一个类(从 ClosureSignatureConflictResolver
继承),如果在初始推断计算完成后找到多个参数类型,则可以执行其他参数类型的解析。Groovy附带一个默认类型解析器,它不执行任何操作,另一个则选择第一个签名(如果找到多个)。只有在找到多个签名并且设计为后处理器时才会调用解析程序。任何需要注入类型信息的语句都必须传递通过类型提示确定的参数签名之一,然后解析器在返回的候选签名中进行选择。
@DelegatesTo
类型检查器使用 @DelegatesTo
注解来推断委托的类型。它允许API设计者指示编译器什么是委托的类型和委派策略。@DelegatesTo
注解将在 特定部分中讨论。
6.6.3. 静态编译
动态vs静态
在类型检查部分,我们已经看到,由于 @TypeChecked
注解,Groovy提供了可选的类型检查。类型检查器在编译时运行,并执行动态代码的静态分析。无论是否启用了类型检查,程序的行为都完全相同。这意味着 @TypeChecked
注解对于程序的语义是中性的。即使可能需要在源代码中添加类型信息以使程序被认为是类型安全的,但最终程序的语义是相同的。
虽然这听起来不错,但实际上存在一个问题:在编译时完成的动态代码的类型检查,如果没有发生特定于运行时的行为才正确。例如,以下程序类型检查通过:
class Computer {
int compute(String str) {
str.length()
}
String compute(int x) {
String.valueOf(x)
}
}
@groovy.transform.TypeChecked
void test() {
def computer = new Computer()
computer.with {
assert compute(compute('foobar')) =='6'
}
}
有两个 compute
方法。一个接受一个 String
并返回一个 int
,另一个接受一个 int
并返回一个 String
。如果你编译它,它被认为是类型安全:compute('foobar')
内部调用将返回一个 int
,并且在此 int
上调用 compute
将依次返回一个 String
。
现在,在调用 test()
之前,请考虑添加以下行:
Computer.metaClass.compute = { String str -> new Date() }
使用运行时元编程,我们实际上是修改了 compute(String)
方法的行为,因此它不会返回提供的参数的长度,而是返回 Date
。如果执行该程序,它将在运行时失败。由于这行可以从任何地方添加,在任何线程中,类型检查器绝对没有办法静态地确保不会发生这样的事情。简而言之,类型检查器易受猴子补丁。这只是一个例子,但这说明了对动态程序进行静态分析本质上是错误的概念。
Groovy语言为 @TypeChecked
提供了一个替代注解,它实际上将确保推断为被调用的方法将在运行时被有效地调用。此注解将Groovy编译器转换为静态编译器,其中所有方法调用在编译时解析并生成的字节码确保发生这种情况:注解为 @groovy.transform.CompileStatic
。
@CompileStatic
注解
可以在任何可以使用 @TypeChecked
注解的地方添加 @CompileStatic
注解,也就是说在类或方法上。没有必要同时添加 @TypeChecked
和 @CompileStatic
,因为 @CompileStatic
执行 @TypeChecked
所执行的所有操作,除此之外还会触发静态编译。
让我们以失败的例子为例,但这一次让我们用 @CompileStatic
替换 @TypeChecked
注解:
class Computer {
int compute(String str) {
str.length()
}
String compute(int x) {
String.valueOf(x)
}
}
@groovy.transform.CompileStatic
void test() {
def computer = new Computer()
computer.with {
assert compute(compute('foobar')) =='6'
}
}
Computer.metaClass.compute = { String str -> new Date() }
test()
这是唯一的区别。如果我们执行这个程序,这次没有运行时错误。test
方法不受猴子补丁的影响,因为在其体内调用的 compute
方法在编译时被链接,因此即使 Computer
的元类更改,程序仍然按类型检查器的预期运行。
主要好处
在代码中使用 @CompileStatic
有几个好处:
-
类型安全
-
对猴子补丁的免疫力
-
性能提升
性能提升取决于你正在执行的程序类型。如果它受I/O限制,静态编译代码和动态代码之间的差异几乎不可察觉。在高CPU密集型代码上,由于生成的字节码与Java等效程序生成的字节码非常接近(如果不相等),因此性能得到极大提高。
使用Groovy的invokedynamic版本(JDK 7及以上版本的用户可以访问该版本),动态代码的性能应该非常接近于静态编译代码的性能。有时,它甚至可以更快!只有一种方法可以确定你应该选择哪个版本:测量。原因是根据你的程序和你使用的JVM,性能可能会有很大差异。特别是,groovy的invokedynamic版本对正在使用的JVM版本非常敏感。