1. Groovy语言规范
1.1. 语法
本章将覆盖Groovy编程语言的语法。 Groovy语言的语法是由JAVA语言的语法衍生出来, 但具体结构上会有所增强,同时容许一定的简化。
1.1.1. 注释
单行注释
单行注释以//
开始,同时它可以出现在行的任何地方。
在//
之后的字符, 一直到行尾都是注释的部分。
// a standalone single line comment
println "hello" // a comment till the end of the line
多行注释
一个多行的注释以/*
开头,同时它可以出现在行的任何地方。
在/*
之后的字符将会被视作注释的一部分,包括新一行的字符, 直到第一个*/
来结束注释。
多行注释因此可以被放到行尾,或者甚至在句子中间。
/* a standalone multiline comment
spanning two lines */
println "hello" /* a multiline comment starting
at the end of a statement */
println 1 /* one */ + 2 /* two */
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 ag reeting message
*/
String greet(String otherPerson) {
"Hello ${otherPerson}"
}
}
GroovyDoc遵循与Java’s自带的JavaDoc一样的约定。 所以你可以使用于JavaDoc一样的标记。
Shebang行
除了单行注释外, 还有一种被特别的行注释, 通常被称作shebang行,它通常在UNIX系统中被认知,它容许脚本直接在命令行中运行那些你已经安装的Groovy和那些已经在PATH
中可用的groovy
命令。
#!/usr/bin/env groovy
println "Hello from the shebang line"
# 字符必须是文件的第一个字符。 任何缩进都会引起一个编译错误。
|
1.1.2. 关键字
下面的列表列出了全部Groovy语言的关键字:
as |
assert |
break |
case |
catch |
class |
const |
continue |
def |
default |
do |
else |
enum |
extends |
false |
finally |
for |
goto |
if |
implements |
import |
in |
instanceof |
interface |
new |
null |
package |
return |
super |
switch |
this |
throw |
throws |
true |
try |
while |
1.1.3. 标识符
普通标识符
标识符以字母,美元符或下划线开头。不能以数字开头
字母可以是以下的范围:
-
a to z (小写 ascii 字母)
-
A to Z (大写 ascii 字母)
-
\u00C0 to \u00D6
-
\u00D8 to \u00F6
-
\u00F8 to \u00FF
-
\u0100 to \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
带引号的标识符
带引号标识符出现在点号表达式的点号之后。
例如,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"
正如我们将会在strings一节看到的那样,Groovy提供不同的字符串文字。 所有跟着点号的字符串都是被允许的:
map.'single quote'
map."double quote"
map.'''triple single quote'''
map."""triple double quote"""
map./slashy string/
map.$/dollar slashy string/$
正如下面的例子,纯字符的字符串和Groovy的GStings(字符串插值)的区别是,计算整个标识符时,插入的字符值是被插入到最终字符串:
def firstname = "Homer"
map."Simson-${firstname}" = "Homer Simson"
assert map.'Simson-Homer' == "Homer Simson"
1.1.4. 字符串
字符以链的形式形成的文本被称作字符串。
Groovy容许你实例化java.lang.String
对象,以及在其他语言中被称作插值字符串(interpolated strings)的GStrings (groovy.lang.GString
)
单引号字符串
单引号字符串是一系列被单引号引起来的字符:
def firstname = "Homer"
map."Simson-${firstname}" = "Homer Simson"
assert map.'Simson-Homer' == "Homer Simson"
单引号字符串是纯的java.lang.String ,同时不支持插值。
|
字符串连接
所有Groovy的字符串都可以用+
号连接起来:
assert 'ab' == 'a' + 'b'
三引号字符串
三引号字符串是一系列被单引号引起来的字符:
'a single quoted string'
三引号字符串是纯的java.lang.String ,同时不支持插值。
|
三引号字符串可以是多行的。 你不必将字符串分割成几块,也不必用连接符或换行符转义字符来将字符串跨行:
def aMultilineString = '''line one
line two
line three'''
如果你的代码是缩进的,比如在类里面的方法,你的字符串将会包含这些空白的缩进。
Groovy开发套件提供String#stripIndent()
方法来剥离(stripping out)缩进,
同时Groovy开发套件提供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'
双引号字符串
双引号字符串是一系列被双引号引起来的字符:
"a double quoted string"
如果没有插值表示式的话,双引号字符串是纯的java.lang.String ,
但如果有插值存在的话,双引号字符串就是一个groovy.lang.GString 实例。
|
为了转义一个双引号,你可以用反斜杠符号:"A double quote: \"" .
|
字符串插值
除了单引号和三引号字符串,任何Groovy表达式都可以插值到所有的字符串里。
插值就是当计算字符串的值时,用值替换掉字符串中的占位符。
占位符表达式是用${}
围起来的,或者以$
为前缀的点表达式。
当一个GString被传到一个需要字符串为参数的方法时,这个GString会调用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()"
}
你可以将"$number.toString()" 想象为被解析器解释执行为"${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 类型变量,然后我们将它插值到两个GString,
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() 方法的签名明确的说明了它的唯一的一个参数是字符串类型 |
5 | 我们也验证参数确实是一个字符串,而不是一个GString。 |
GString和String的hashCode
虽然插值字符串可以被用来替代纯的Java字符串, 但它们有特别的方法来区分字符串:它们的hashcode是不同的。 纯Java字符串是不可变的,而GString表示的结果字符串却是可变的,这取决与它的插值。 即使是相同的结果字符串,GString和String的hasCode也是不同的。
assert "one: ${1}".hashCode() != "one: 1".hashCode()
GString和String有着不同的hasCode值,应该避免把GString用作Map的键,尤其是当我们尝试检索一个值时,应当使用String而不是GString。
def key = "a"
def m = ["${key}": "letter ${key}"] (1)
assert m["a"] == null (2)
1 | map创建了一个以GString为键的键值对 |
2 | 当我们尝试用String类型键获取值的时候,我们将找不到, 因为String和GString有着不同的hashCode值 |
三双引号字符串
三双引号字符串表现得像双引号字符串,只是它像三单引号字符串那样,是多行的。
def name = 'Groovy'
def template = """
Dear Mr ${name},
You're the winner of the lottery!
Yours sincerly,
Dave
"""
assert template.toString().contains('Groovy')
在三双引号字符串里的,无论是双引号还是单引号都需要转义的。 |
斜杠字符串
除了通常的带引号的字符串,Groovy提供斜杠字符串,它使用/
作为分隔符。
斜杠字符串在定义正则表达式和模式的时候非常有用,因为不必要反斜杠来转义。
斜杠字符串的例子:
def fooPattern = /.*foo.*/
assert fooPattern == '.*foo.*'
只有正斜杠需要用反斜杠转义:
def escapeSlash = /The character \/ is a forward slash/
assert escapeSlash == 'The character / is a forward slash'
斜杠字符串可以是多行的:
def multilineSlashy = /one
two
three/
assert multilineSlashy.contains('\n')
斜杠字符串也可以被插值(例如GString):
def color = 'blue'
def interpolatedSlashy = /a ${color} car/
assert interpolatedSlashy == 'a blue car'
有几个陷阱需要注意的。
一个空的斜杠字符串不可以用两个正斜杠来表示,因为它会被Groovy解析器理解为是一个行注释。 这就是以下的断言为什么实际上不会被编译,因为它看起像个还没结束的句子:
assert '' == //
美元符斜杠字符串
美元符斜杠字符串是以$/
开始和以/$
结束的多行的GString。
转义的字符是美元符号,美元符号可以转义其他美元符号,或者正斜杠。
但美元符和正斜杠不是一定要去转义,
只要转义那些好像GString占位符那样以美元符开始的字符串,
或者那些以关闭美元斜杠字符串分割符号为开始的字符串。
这里有一个例子:
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 dollar slashy string delimiter
/$
assert [
'Guillaume',
'April, 1st',
'$ dollar sign',
'$ escaped dollar sign',
'\\ backslash',
'/ forward slash',
'$/ escaped forward slash',
'/$ escaped dollar slashy string delimiter'
].each { dollarSlashy.contains(it) }
字符串汇总表
字符串名 |
字符串语法 |
插值 |
多行 |
转义字符 |
单引号 |
|
否 |
否 |
|
三单引号 |
|
否 |
是 |
|
双引号 |
|
是 |
否 |
|
三双引号 |
|
是 |
是 |
|
斜杠 |
|
是 |
是 |
|
美元符斜杠 |
|
是 |
是 |
|
字符
与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 | 使用chat操作符强制转换 |
当一个变量持有一个字符时,更倾向于第1种选择, 当一个字符值必须传给一个方法调用的时候,更倾向于其他两种(2 and 3)选择。 |
1.1.5. 数字
Groovy支持不同种类整型和十进制文本,由Java的Number
类型支持。
整型类型
整型类型与Java的是一样的:
-
byte
-
char
-
short
-
int
-
long
-
java.lang.BigInteger
你可以用下面的声明创建这些类型的整数:
// 基本类型
byte b = 1
char c = 2
short s = 3
int i = 4
long l = 5
// 无限精度
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
可选择的基于非10进制的表示
在Java6和之前,以及在Groovy中,数字可以用十进制,八进制和十六进制来表示,
而在Java7和Groovy2,你可以使用带有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
小数
小数类型与Java的一样:
-
float
-
double
-
java.lang.BigDecimal
你可以用以下声明来创建这些小数类型数字:
// primitive types
float f = 1.234
double d = 2.345
// infinite precision
BigDecimal bd = 3.456
小数可以使用指数,使用e
or 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
作为参数的方法或闭包中的。
小数不能使用二进制,八进制或十六进制表示形式来表示。 |
在数字中的下划线
当在输入一个很长的数字是,是很难看清楚这些数字的, 通过容许在数字之间输入下划线来分组,这样可以更容易分清楚这些数字:
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
Number类型的后缀
我们可以用指定后缀(看下表)的方式强制指定一个数字的类型(包括二进制,八进制和十六进制),而这些后缀是不区分大小写的。
类型 | 后缀 |
---|---|
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
算术运算
虽然操作符将会在后面详细介绍,但讨论算术运算符的行为和运算结果的类型也是非常重要的。
除了除法和指数这两个二元操作符外(后面将会介绍),
-
byte
,char
,short
和int
之间的二元运算结果将会是int
-
涉及到
long
与byte
,char
,short
和int
的结果将会是long
-
涉及到
BigInteger
和其他整型类型的结果将会是BigInteger
-
float
,double
和BigDecimal
之间二元操作的结果将会是double
-
两个
BigDecimal
之间的二元操作结果将会是BigDecimal
下面的表格总结了这些规则:
byte | char | short | int | long | BigInteger | float | double | BigDecimal | |
---|---|---|---|---|---|---|---|---|---|
byte |
int |
int |
int |
int |
long |
BigInteger |
double |
double |
double |
char |
int |
int |
int |
long |
BigInteger |
double |
double |
double |
|
short |
int |
int |
long |
BigInteger |
double |
double |
double |
||
int |
int |
long |
BigInteger |
double |
double |
double |
|||
long |
long |
BigInteger |
double |
double |
double |
||||
BigInteger |
BigInteger |
double |
double |
double |
|||||
float |
double |
double |
double |
||||||
double |
double |
double |
|||||||
BigDecimal |
BigDecimal |
得益于Groovy的操作符重载,常用的算术运算符一样可以用于BigInteger 和 BigDecimal 之间,而不用好像Java那样你必须使用显式方法来操作这些数字。
|
除法运算符的情况
如果其中一个操作数是float
或double
的话,除法运算/
(和 /=
执行除法和指定)的结果将会是double
,其他的都会是BigDecimal
(当操作数是short
, char
, byte
, int
, long
,BigInteger
或BigDecimal
之间的任意组合)。
BigDecimal
division is performed with the divide()
method if the division is exact
(ie. yielding a result that can be represented within the bounds of the same precision and scale),
or using a MathContext
with a precision
of the maximum of the two operands’ precision plus an extra precision of 10,
and a scale
of the maximum of 10 and the maximum of the operands’ scale.
对于像Java中的整数除法,你应该使用intdiv() 方法,而Groovy中并没有提供专用的整数除法运算符。
|
幂运算的情况下
幂运算是用**
操作符表示,该操作符带有两个参数:底数和指数。
幂运算的结果过取决与他的操作数和运算的结果(尤其是如果结果可被表示为一个整数)
以下是Groovy的幂运算的规则,以确定结果类型:
-
如果指数是一个十进制值
-
如果结果可被表示为一个
Integer
,则返回一个Integer
-
否则,如果结果可以表示为一个
Long
,然后返回一个Long
-
否则返回一个
Double
-
-
如果指数为整数
-
当指数为负,则返回一个
Integer
,Long
或Double
,这取决与结果值是否合适那个类型 -
如果该指数是正数或零
-
如果底数是一个
BigDecimal
,则返回一个BigDecimal
值的结果 -
如果底数是一个
BigInteger
,然后返回一个BigInteger
结果值 -
如果底数是一个整数,而且结果可以放进一个
Integer
类型的话,就返回Integer
,否则返回一个BigInteger
-
如果底数是一个
Long
,而且结果可以放进一个Long
类型的话,就返回Long
,否则返回一个BigInteger
-
-
我们可以用一些举例来说明这些规则:
// base and exponent are ints and the result can be represented by an Integer
assert 2 ** 3 instanceof Integer // 8
assert 10 ** 9 instanceof Integer // 1_000_000_000
// the base is a long, so fit the result in a Long
// (although it could have fit in an Integer)
assert 5L ** 2 instanceof Long // 25
// the result can't be represented as an Integer or Long, so return a BigInteger
assert 100 ** 10 instanceof BigInteger // 10e20
assert 1234 ** 123 instanceof BigInteger // 170515806212727042875...
// the base is a BigDecimal and the exponent a negative int
// but the result can be represented as an Integer
assert 0.5 ** -2 instanceof Integer // 4
// the base is an int, and the exponent a negative float
// but again, the result can be represented as an Integer
assert 1 ** -0.3f instanceof Integer // 1
// the base is an int, and the exponent a negative int
// but the result will be calculated as a Double
// (both base and exponent are actually converted to doubles)
assert 10 ** -1 instanceof Double // 0.1
// the base is a BigDecimal, and the exponent is an int, so return a BigDecimal
assert 1.2 ** 10 instanceof BigDecimal // 6.1917364224
// the base is a float or double, and the exponent is an int
// but the result can only be represented as a Double value
assert 3.4f ** 5 instanceof Double // 454.35430372146965
assert 5.6d ** 2 instanceof Double // 31.359999999999996
// the exponent is a decimal value
// and the result can only be represented as a Double value
assert 7.8 ** 1.9 instanceof Double // 49.542708423868476
assert 2 ** 0.1f instanceof Double // 1.0717734636432956
1.1.6. Booleans
布尔值是用来表示真值的特殊数据类型:true
和false
。
可使用这种数据类型,简单地跟踪真/假的条件(conditions)。
布尔值就像任何其他数据类型一样,可以存储在变量中,可以被指派到字段,:
def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true
true
和false
是仅有的两个布尔值基元。
但更复杂的布尔表达式可以使用逻辑运算符来表示。
此外,Groovy中具有强制非布尔对象转化为布尔值的特殊规则(通常被称为Groovy Truth)。
1.1.7. 列表
Groovy使用逗号分割列表的值,使用方括号括起,来表示列表。
Groovy列表是普通的JDKjava.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 | 访问列表的第一个元素(由0来是算起) |
2 | 用一个负数索引来访问最尾的元素:-1是从尾部算起的第一个元素 |
3 | 使用等号来设置一个新的值到列表的第三个元素 |
4 |
使用<< 左移动操作符添加一个元素到列表的最后
|
5 | 一次访问两个元素,返回一个包含这两个元素的新列表 |
6 | 使用一个从开始到结束元素的位置的范围来访问列表的一个范围内的值。 |
如列表可以是异构性质的,列表还可以包含其他的列表来创建多维表:
def multi = [[0, 1], [2, 3]] (1)
assert multi[1][0] == 2 (2)
1 | 定义一个以列表作为元素的列表 |
2 | 访问最上面列表中的第二个元素同时是内部列表中的第一个元素的元素 |
1.1.8. 数组
Groovy中重用列表符号表示数组,但创建这样的文字数组,你需要通过强制或类型声明,明确地定义数组类型。
String[] arrStr = ['Ananas', 'Banana', 'Kiwi'] (1)
assert arrStr instanceof String[] (2)
assert !(arrStr instanceof List) (3)
def numArr = [1, 2, 3] as int[] (4)
assert numArr instanceof int[] (5)
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.1.9. 映射(Maps)
在其他语言中有时也被称为字典或关联数组的特性在Groovy称为映射(maps)。 映射(maps)关联键到值,用冒号来分隔键到值,用逗号分割每对键值对,同时用方括号把整个键值包起来。
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'
assert colors instanceof java.util.LinkedHashMap
1 | 我们定义了一个颜色名字和他们十六进制编码的html颜色相关联的映射 |
2 |
我们使用下标符号来检索与red 键关联的内容
|
3 | 我们也可以使用属性表示法来断言绿色的十六进制表示 |
4 | 相似地,我们可以使用下标符号添加新的键/值对 |
5 | 或者属性访问符来添加yellow 颜色 |
当使用该键的名字时,我们实质上是定义在映射中定义字符串键。 |
Groovy建立的maps实际上是java.util.LinkedHashMap 的实例。
Groovy creates maps that are actually instances of java.util.LinkedHashMap .
|
如果你尝试用一个不存在的健来访问map:
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 | The key associated with the 'Guillaume’ name will actually be the "key" string, not the value associated with the key variable |
2 | The map doesn’t contain the 'name’ key |
3 | Instead, the map contains a 'key’ key |
You can also pass quoted strings as well as keys: ["name": "Guillaume"] .
This is mandatory if your key string isn’t a valid identifier,
for example if you wanted to create a string key containing a hash like in: ["street-name": "Main street"] .
|
When you need to pass variable values as keys in your map definitions, you must surround the variable or expression with parentheses:
person = [(key): 'Guillaume'] (1)
assert person.containsKey('name') (2)
assert !person.containsKey('key') (3)
1 | This time, we surround the key variable with parentheses, to instruct the parser we are passing a variable rather than defining a string key |
2 | The map does contain the name key |
3 | But the map doesn’t contain the key key as before |
1.2. 运算符
本章涵盖了Groovy的所有运算符.
1.2.1. 算术运算符
Groovy支持在数学和其他编程语言如Java中常见的算术运算符.Java中所有的算术运算符都能够被支持.让我们通过随后的例子来认识他们.
常见算术运算符
下列的二进制算术运算符在Groovy中是有效的:
运算符 | 作用 | 备注 |
---|---|---|
|
加 |
|
|
减 |
|
|
乘 |
|
|
除 |
|
|
模 |
|
|
幂 |
在这里the power operation可以看到更多关于运算符返回类型的信息. |
这里有几个使用这些运算符的示例:
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
一元运算符
+
和-
也可以用作一元运算符:
assert +3 == 3
assert -4 == 0 - 4
assert -(-1) == 1 (1)
1 | Note the usage of parentheses to surround an expression to apply the unary minus to that surrounded expression. |
依据一元算术表达式,++
(叠加)和--
(叠减)是有效的,无论是前缀符号还是后缀符号:
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 | 后缀++ 将会在a 执行和分配完值到b 中之后自增1. |
2 | 后缀-- 将会在c 执行和分配完值到d 中之后自减1. |
3 | 前缀++ 将会在e 执行和分配值到f 中之前自加1. |
4 | 前缀-- 将会在g 执行和分配值到h 中之前自减1. |
赋值运算符
如上所见的二进制算术运算符,如下一些写法同样是有效的:
-
+=
-
-=
-
*=
-
/=
-
%=
实际使用示例:
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
1.2.2. 关系运算符
关系运算符允许对象之间比较,从而知道两个对象是相同还是不同,一个是大于还是小于或等于另一个.
下列运算符是有效的:
运算符 | 作用 |
---|---|
|
相等 |
|
不等 |
|
小于 |
|
小于或等于 |
|
大于 |
|
大于或等于 |
如下是这些运算符在简单数字上的使用:
assert 1 + 2 == 3
assert 3 != 4
assert -2 < 3
assert 2 <= 2
assert 3 <= 4
assert 5 > 1
assert 5 >= -2
1.2.3. 逻辑运算符
Groovy提供三个boolean值的逻辑运算符:
-
&&
: 逻辑 "并" -
||
: 逻辑 "或" -
!
: 逻辑 "非"
让我们举例说明他们的用法:
assert !false (1)
assert true && true (2)
assert true || false (3)
1 | "非"false即为true |
2 | true "并" true 仍为 true |
3 | true "或" false 为 true |
优先级
逻辑"非"比逻辑"并"的优先级更高.
assert !false && true (1)
1 | 这里的结果为true,因为"非"比"并"拥有更高的优先级,否则,这个断言将会失败. |
逻辑"并"比逻辑"或"拥有更高的优先级.
assert false || true && true (1)
1 | 这里的结果为true,因为"并"比"或"拥有更高的优先级,否则,这个断言将会失败. |
短路的(||
)
逻辑运算符"或"支持短路:如果左边的操作数为true,那么就不会再验证右边的操作数。只有当左边的操作数为false时右边的操作数才会被验证.
called = false
boolean somethingTrueOrFalse(boolean b) { (1)
called = true
return b
}
assert true || somethingTrueOrFalse(false)
assert !called (2)
assert false || somethingTrueOrFalse(true)
assert called (3)
1 | 我们创建一个方法返回boolean类型参数,方法内部设置了called 的值. |
2 | 在第一个案例中,刚刚的方法没有被调用,因为|| 的左边发生了短路. |
3 | 在第二个案例中,方法被调用了,事实证明flag现在的值为true. |
1.2.4. Bitwise operators
Groovy offers 4 bitwise operators:
-
&
: bitwise "and" -
|
: bitwise "or" -
^
: bitwise "xor" (exclusive "or") -
~
: bitwise negation
Bitwise operators can be applied on a byte
or an int
and return an 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 | bitwise and |
2 | bitwise and returns common bits |
3 | bitwise or |
4 | bitwise or returns all 1 bits |
5 | setting a mask to check only the last 8 bits |
6 | bitwise exclusive or on self returns 0 |
7 | bitwise exclusive or |
8 | bitwise negation |
It’s worth noting that the internal representation of primitive types follow the Java Language Specification. In particular, primitive types are signed, meaning that for a bitwise negation, it is always good to use a mask to retrieve only the necessary bits.
In Groovy, bitwise operators have the particularity of being overloadable, meaning that you can define the behavior of those operators for any kind of object.
1.2.5. Conditional operators
Not operator
The "not" operator is represented with an exclamation mark (!
) and inverts the result of the underlying boolean expression. In
particular, it is possible to combine the not
operator with the Groovy truth:
assert (!true) == false (1)
assert (!'foo') == false (2)
assert (!'') == true (3)
1 | the negation of true is false |
2 | foo is a non empty string, evaluating to true , so negation returns false |
3 | '' is an empty string, evaluating to false , so negation returns true |
Ternary operator
The ternary operator is a shortcut expression that is equivalent to an if/else branch assigning some value to a variable.
Instead of:
if (string!=null && string.length()>0) {
result = 'Found'
} else {
result = 'Not found'
}
You can write:
result = (string!=null && string.length()>0)?'Found':'Not found'
The ternary operator is also compatible with the Groovy truth, so you can make it even simpler:
result = string?'Found':'Not found'
Elvis operator
The "Elvis operator" is a shortening of the ternary operator. One instance of where this is handy is for returning
a sensible default value if an expression resolves to false
or null
. A simple example might look like this:
displayName = user.name ? user.name : 'Anonymous' (1)
displayName = user.name ?: 'Anonymous' (2)
1 | with the ternary operator, you have to repeat the value you want to assign |
2 | with the Elvis operator, the value which is tested is used if it is not false or null |
Usage of the Elvis operator reduces the verbosity of your code and reduces the risks of errors in case of refactorings, by removing the need to duplicate the expression which is tested in both the condition and the positive return value.
1.2.6. Object operators
Safe navigation operator
The Safe Navigation operator is used to avoid a NullPointerException
. Typically when you have a reference to an object
you might need to verify that it is not null
before accessing methods or properties of the object. To avoid this, the safe
navigation operator will simply return null
instead of throwing an exception, like so:
def person = Person.find { it.id == 123 } (1)
def name = person?.name (2)
assert name == null (3)
1 | find will return a null instance |
2 | use of the null-safe operator prevents from a NullPointerException |
3 | result is null |
Direct field access operator
Normally in Groovy, when you write code like this:
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 | public field name |
2 | a getter for name that returns a custom string |
3 | calls the getter |
The user.name
call triggers a call to the property of the same name, that is to say, here, to the getter for name
. If
you want to retrieve the field instead of calling the getter, you can use the direct field access operator:
assert user.@name == 'Bob' (1)
1 | use of .@ forces usage of the field instead of the getter |
Method reference operator
The method reference operator (.&
) call be used to store a reference to a method in a variable, in order to call it
later:
def str = 'example of method reference' (1)
def fun = str.&toUpperCase (2)
def upper = fun() (3)
assert upper == str.toUpperCase() (4)
1 | the str variable contains a String |
2 | we store a reference to the toUpperCase method on the str instance inside a variable named fun |
3 | fun can be called like a regular method |
4 | we can check that the result is the same as if we had called it directly on str |
There are multiple advantages in using method references. First of all, the type of such a method reference is
a groovy.lang.Closure
, so it can be used in any place a closure would be used. In particular, it is suitable to
convert an existing method for the needs of the strategy pattern:
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 | the transform method takes each element of the list and calls the action closure on them, returning a new list |
2 | we define a function that takes a Person a returns a String |
3 | we create a method reference on that function |
4 | we create the list of elements we want to collect the descriptors |
5 | the method reference can be used where a Closure was expected |
Method references are bound by the receiver and a method name. Arguments are resolved at runtime, meaning that if you have multiple methods with the same name, the syntax is not different, only resolution of the appropriate method to be called will be done at runtime:
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 | define an overloaded doSomething method accepting a String as an argument |
2 | define an overloaded doSomething method accepting an Integer as an argument |
3 | create a single method reference on doSomething , without specifying argument types |
4 | using the method reference with a String calls the String version of doSomething |
5 | using the method reference with an Integer calls the Integer version of doSomething |
1.2.7. Regular expression operators
Pattern operator
The pattern operator (~
) provides a simple way to create a java.util.regex.Pattern
instance:
def p = ~/foo/
assert p instanceof Pattern
while in general, you find the pattern operator with an expression in a slashy-string, it can be used with any kind of
String
in Groovy:
p = ~'foo' (1)
p = ~'foo' (2)
p = ~$/dollar/slashy $ string/$ (3)
p = ~"${pattern}" (4)
1 | using single quote strings |
2 | using double quotes strings |
3 | the dollar-slashy string lets you use slashes and the dollar sign without having to escape them |
4 | you can also use a GString! |
Find operator
Alternatively to building a pattern, you can directly use the find operator =~
to build a java.util.regex.Matcher
instance:
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 | ~= creates a matcher against the text variable, using the pattern on the right hand side |
2 | the return type of ~= is a Matcher |
3 | equivalent to calling if (!m.find()) |
Since a Matcher
coerces to a boolean
by calling its find
method, the =~
operator is consistent with the simple
use of Perl’s =~
operator, when it appears as a predicate (in if
, while
, etc.).
Match operator
The match operator (==~
) is a slight variation of the find operator, that does not return a Matcher
but a boolean
and requires a strict match of the input string:
m = text ==~ /match/ (1)
assert m instanceof Boolean (2)
if (m) { (3)
throw new RuntimeException("Should not reach that point!")
}
1 | ~== matches the subject with the regular expression, but match must be strict |
2 | the return type of ~== is therefore a boolean |
3 | equivalent to calling if (text ==~ /match/) |
1.2.8. Other operators (WIP)
Spread operator
The Spread Operator (*.
) is used to invoke an action on all items of an aggregate object. It is equivalent to calling the action on each item
and collecting the result into a list:
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 | build a list of Car items. The list is an aggregate of objects. |
2 | call the spread operator on the list, accessing the make property of each item |
3 | returns a list of strings corresponding to the collection of make items |
The spread operator is null-safe, meaning that if an element of the collection is null, it will return null instead of throwing a 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 | build a list for which of of the elements is null |
2 | using the spread operator will not throw a NullPointerException |
3 | the receiver might also be null, in which case the return value is null |
The spread operator can be used on any class which implements the Iterable
interface:
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']
Spreading method arguments
There may be situations when the arguments of a method call can be found in a list that you need to adapt to the method arguments. In such situations, you can use the spread operator to call the method. For example, imagine you have the following method signature:
int function(int x, int y, int z) {
x*y+z
}
then if you have the following list:
def args = [4,5,6]
you can call the method without having to define intermediate variables:
assert function(*args) == 26
It is even possible to mix normal arguments with spread ones:
args = [4]
assert function(*args,5,6) == 26
Spread list elements
When used inside a list literal, the spread operator acts as if the spread element contents were inlined into the list:
def items = [4,5] (1)
def list = [1,2,3,*items,6] (2)
assert list == [1,2,3,4,5,6] (3)
1 | items is a list |
2 | we want to insert the contents of the items list directly into list without having to call addAll |
3 | the contents of items has been inlined into list |
Spread map elements
The spread map operator works in a similar manner as the spread list operator, but for maps. It allows you to inline the contents of a map into another map literal, like in the following example:
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 is the map that we want to inline |
2 | we use the *:m1 notation to spread the contents of m1 into map |
3 | map contains all the elements of m1 |
The position of the spread map operator is relevant, like illustrated in the following example:
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 is the map that we want to inline |
2 | we use the *:m1 notation to spread the contents of m1 into map , but redefine the key d after spreading |
3 | map contains all the expected keys, but d was redefined |
Range operator
Groovy supports the concept of ranges and provides a notation (..
) to create ranges of objects:
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 | a simple range of integers, stored into a local variable |
2 | an IntRange , with inclusive bounds |
3 | an IntRange , with exclusive upper bound |
4 | a groovy.lang.Range implements the List interface |
5 | meaning that you can call the size method on it |
Ranges implementation is lightweight, meaning that only the lower and upper bounds are stored. You can create a range
from any Comparable
object. For example, you can create a range of characters this way:
assert ('a'..'d').collect() == ['a','b','c','d']
Spaceship operator
The spaceship operator (<=>
) delegates to the compareTo
method:
assert (1 <=> 1) == 0
assert (1 <=> 2) == -1
assert (2 <=> 1) == 1
assert ('a' <=> 'z') == -1
Subscript operator
The subscript operator is a short hand notation for getAt
or putAt
, depending on whether you find it on
the left hand side or the right hand side of an assignment:
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] can be used instead of getAt(2) |
2 | if on left hand side of an assignment, will call putAt |
3 | getAt also supports ranges |
4 | so does putAt |
5 | the list is mutated |
The subscript operator, in combination with a custom implementation of getAt
/putAt
is a convenient way for destructuring
objects:
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 | the User class defines a custom getAt implementation |
2 | the User class defines a custom putAt implementation |
3 | create a sample user |
4 | using the subscript operator with index 0 allows retrieving the user id |
5 | using the subscript operator with index 1 allows retrieving the user name |
6 | we can use the subscript operator to write to a property thanks to the delegation to putAt |
7 | and check that it’s really the property name which was changed |
Membership operator
The membership operator (in
) is equivalent to calling the isCase
method. In the context of a List
, it is equivalent
to calling contains
, like in the following example:
def list = ['Grace','Rob','Emmy']
assert ('Emmy' in list) (1)
1 | equivalent to calling list.contains('Emmy') or list.isCase('Emmy') |
Identity operator
In Groovy, using ==
to test equality is different from using the same operator in Java. In Groovy, it is calling equals
.
If you want to compare reference equality, you should use is
like in the following example:
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 | Create a list of strings |
2 | Create another list of strings containing the same elements |
3 | using == , we test object equality |
4 | but using is , we can check that references are distinct |
Coercion operator
The coercion operator (as
) is a variant of casting. Coercion converts object from one type to another without them
being compatible for assignement. Let’s take an example:
Integer x = 123
String s = (String) x (1)
1 | Integer is not assignable to a String , so it will produce a ClassCastException at runtime |
This can be fixed by using coercion instead:
Integer x = 123
String s = x as String (1)
1 | Integer is not assignable to a String , but use of as will coerce it to a String |
When an object is coerced into another, unless the target type is the same as the source type, coercion will return a
new object. The rules of coercion differ depending on the source and target types, and coercion may fail if no conversion
rules are found. Custom conversion rules may be implemented thanks to the asType
method:
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 | the User class defines a custom conversion rule from User to Identifiable |
2 | we create an instance of User |
3 | we coerce the User instance into an Identifiable |
4 | the target is an instance of Identifiable |
5 | the target is not an instance of User anymore |
Diamond operator
The diamond operator (<>
) is a syntactic sugar only operator added to support compatibility with the operator of the
same name in Java 7. It is used to indicate that generic types should be inferred from the declaration:
List<String> strings = new LinkedList<>()
In dynamic Groovy, this is totally unused. In statically type checked Groovy, it is also optional since the Groovy type checker performs type inference whether this operator is present or not.
Call operator
The call operator ()
is used to call a method named call
implicitly. For any object which defines a call
method,
you can omit the .call
part and use the call operator instead:
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 defines a method named call . Note that it doesn’t need to implement java.util.concurrent.Callable |
2 | we can call the method using the classic method call syntax |
3 | or we can omit .call thanks to the call operator |
1.2.9. Operator precedence (TBD)
1.2.10. Operator overloading
Groovy allows you to overload the various operators so that they can be used with your own classes. Consider this simple class:
class Bucket {
int size
Bucket(int size) { this.size = size }
Bucket plus(Bucket other) { (1)
return new Bucket(this.size + other.size)
}
}
1 | Bucket implements a special method called plus() |
Just by implementing the plus()
method, the Bucket
class can now be used with the +
operator like so:
def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15 (1)
1 | The two Bucket objects can be added together with the + operator |
All (non-comparator) Groovy operators have a corresponding method that you can implement in your own classes. The only requirements are that your method is public, has the correct name, and has the correct number of arguments. The argument types depend on what types you want to support on the right hand side of the operator. For example, you could support the statement
assert (b1 + 11).size == 15
by implementing the plus()
method with this signature:
Bucket plus(int capacity) {
return new Bucket(this.size + capacity)
}
Here is a complete list of the operators and their corresponding methods:
Operator | Method | Operator | Method |
---|---|---|---|
|
a.plus(b) |
|
a.getAt(b) |
|
a.minus(b) |
|
a.putAt(b, c) |
|
a.multiply(b) |
|
a.leftShift(b) |
|
a.div(b) |
|
a.rightShift(b) |
|
a.mod(b) |
|
a.next() |
|
a.power(b) |
|
a.previous() |
|
a.or(b) |
|
a.positive() |
|
a.and(b) |
|
a.negative() |
|
a.xor(b) |
|
a.bitwiseNegative() |
1.3. Program structure (Anto Aravinth)
This chapter covers the program structure of the Groovy programming language.
1.3.1. Package name (TBD)
1.3.2. Imports (TBD)
Default imports (TBD)
Simple import (TBD)
Star import (TBD)
Static import (TBD)
Static star import (TBD)
Import aliasing (TBD)
1.3.3. Scripts versus classes
Package name plays exactly the same role as they used to play in Java. They allows us to separate the code base without any conflicts. Groovy classes must specify their package before the class definition, else the default package is assumed.
Defining a package is very similar to java, as you can see
//Defines a package named com.yoursite
package com.yoursite
To refer the class Foo
in the com.yoursite.com
package then you need to use com.yoursite.com.Foo
in your code else you can use import
statement as we can see now.
1.3.4. Imports
In order to refer any class you need a reference for it. Groovy follows Java’s notion of allowing import
statement to resolve the class references. Groovy provides several builder classes,
one such class is MarkupBuilder
. MarkupBuilder
is inside the package groovy.xml
, in order to use this class, you need to import
it as shown:
//imports the class MarkupBuilder
import groovy.xml.MarkupBuilder
//uses the imported class to create an object
def xml = new MarkupBuilder( )
assert xml != null
Default imports
Default imports are the imports that Groovy language provides by default. For example look at the following code:
new Date()
The same code in Java needs an import statement to Date
class like this: import java.util.Date
. Groovy by default imports these classes for you. There are six packages that groovy imports for you, they are:
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
Simple import
Simple import is an import, where you fully define the class name along with the package. For example the import statement import groovy.xml.MarkupBuilder
in the code below is the simple import, which directly refer the class inside the package.
//imports the class MarkupBuilder
import groovy.xml.MarkupBuilder
//uses the imported class to create an object
def xml = new MarkupBuilder( )
assert xml != null
Star import
Groovy (like Java) provides a special way to import all classes from a package using *
, they are called as Star import. MarkupBuilder
is class which is in package groovy.xml
, another class called StreamingMarkupBuilder
is in the same package. Consider the case, where you need to use two classes, then you can do:
import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder
def markupBuilder = new MarkupBuilder( )
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
Thats a perfectly valid code. But with *
import, we can do the same like this:
import groovy.xml.*
def markupBuilder = new MarkupBuilder( )
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
where, does imports all the class under package
groovy.xml
. Using import can clutters your local namespace. But with groovy type aliasing, this can be solved easily.
Static import
Groovy’s static import capability allows you to reference imported classes as if they were static methods in your own class. This is similar to Java’s static import capability but works with Java 1.4 and above and is a little more dynamic than Java in that it allows you to define methods with the same name as an imported method as long as you have different types. If you have the same types, the imported class takes precedence. Here is a sample of its usage:
import static Boolean.FALSE
assert !FALSE //use directly, without Boolean prefix!
As you can see, now we can able to refer the static variable FALSE
in our code base cleanly.
Static imports along with as
keyword can make elegant solution. Consider you want to get the Calendar
instance, using getInstance()
method. Its a static method, so we can do a static import. But instead of calling getInstance()
everytime, we can do this which can increase code readability:
import static Calendar.getInstance as now
assert now().class == Calendar.getInstance().class
Thats clean and sweet!
Static star import
Its very similar to the star import that we have seen earlier. It will import all the static methods from the given class. For example, lets say we need to calculate sin and cos for our application.
The class java.lang.Math
has static methods named sin
,cos
which fits our need. With help of static star import, we can do:
import static java.lang.Math.*
assert sin(0) == 0.0
assert cos(0) == 1.0
As you can see, we were able to access the methods sin
,cos
directly, instead like Math.sin
.
Import aliasing
With type aliasing, we can refer to a fully qualified class name with the name of your choice. This can be done with as
keyword in Groovy.
Consider the class with the following library code:
package thirdpartylib
public class MultiplyTwo {
def static multiply(def value) {
return value * 3 //intentionally wrong.
}
}
and we have some existing code using this library:
def result = new MultiplyTwo().multiply(2)
Obviously, the result
variable contains the wrong result. And assume you have used this library through out your code-base. Groovy has an elegant way to fix this issue.
With the type aliasing in place, we can fix the bug using the below code:
import thirdpartylib.MultiplyTwo as OrigMultiplyTwo
class MultiplyTwo extends OrigMultiplyTwo {
def multiply(def value)
{
return value * 2 //corrected here
}
}
// nothing changes below here
def multiplylib = new MultiplyTwo()
//assert passes as well
assert 4 == new MultiplyTwo().multiply(2)
Thats it! Notice how Groovy allowed us to use as
keyword to solve the typical problem.
With Type aliasing we can able to solve the name collision problem ( which can occur with *
imports) elegantly.
1.3.5. Initializers (TBD)
Static initializers (TBD)
Instance initializers (TBD)
1.4. Object orientation
This chapter covers the object orientation of the Groovy programming language.
1.4.1. Types (TBD)
Primitive types (TBD)
Class (TBD)
Normal class (TBD)
Static class (TBD)
Inner class (TBD)
Abstract class (TBD)
Interface (TBD)
Annotation (TBD)
Closure annotation parameters (TBD)
Meta-annotations (TBD)
Annotation placement (TBD)
Constructors (TBD)
Named argument constructor (TBD)
Methods (TBD)
Method definition (TBD)
Named arguments (TBD)
Default arguments (TBD)
Varargs (TBD)
Method selection algorithm (TBD)
Exception declaration (TBD)
Fields and properties (TBD)
Fields (TBD)
Properties (TBD)
Inheritance (TBD)
Generics (TBD)
1.4.2. Traits
Traits are a a structural construct of the language which allow:
-
composition of behaviors
-
runtime implementation of interfaces
-
behavior overriding
-
compatibility with static type checking/compilation
They can be seen as interfaces carrying both default implementations and state. A trait is defined using the
trait
keyword:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | declaration of a trait |
2 | declaration of a method inside a trait |
Then it can be used like a normal interface using the implements
keyword:
class Bird implements FlyingAbility {} (1)
def b = new Bird() (2)
assert b.fly() == "I'm flying!" (3)
1 | Adds the trait FlyingAbility to the Bird class capabilities |
2 | instantiate a new Bird |
3 | the Bird class automatically gets the behavior of the FlyingAbility trait |
Traits allow a wide range of capabilities, from simple composition to testing, which are described throughfully in this section.
Methods
Public methods
Declaring a method in a trait can be done like any regular method in a class:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | declaration of a trait |
2 | declaration of a method inside a trait |
Abstract methods
In addition, traits may declare abstract methods too, which therefore need to be implemented in the class implementing the trait:
trait Greetable {
abstract String name() (1)
String greeting() { "Hello, ${name()}!" } (2)
}
1 | implementing class will have to declare the name method |
2 | can be mixed with a concrete method |
Then the trait can be used like this:
class Person implements Greetable { (1)
String name() { 'Bob' } (2)
}
def p = new Person()
assert p.greeting() == 'Hello, Bob!' (3)
1 | implement the trait Greetable |
2 | since name was abstract, it is required to implement it |
3 | then greeting can be called |
Private methods
Traits may also define private methods. Those methods will not appear in the trait contract interface:
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 | define a private method greetingMessage in the trait |
2 | the public greet message calls greetingMessage by default |
3 | create a class implementing the trait |
4 | greet can be called |
5 | but not greetingMessage |
Traits only support public and private methods. Neither protected nor package private scopes are
supported.
|
The meaning of this
this
represents the implementing instance. Think of a trait as a superclass. This means that when you write:
trait Introspector {
def whoAmI() { this }
}
class Foo implements Introspector {}
def foo = new Foo()
then calling:
foo.whoAmI()
will return the same instance:
assert foo.whoAmI().is(foo)
Interfaces
Traits may implement interfaces, in which case the interfaces are declared using the implements
keyword:
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 | declaration of a normal interface |
2 | add Named to the list of implemented interfaces |
3 | declare a class that implements the Greetable trait |
4 | implement the missing greet method |
5 | the greeting implementation comes from the trait |
6 | make sure Person implements the Named interface |
7 | make sure Person implements the Greetable trait |
Properties
A trait may define properties, like in the following example:
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 | declare a property name inside a trait |
2 | declare a class which implements the trait |
3 | the property is automatically made visible |
4 | it can be accessed using the regular property accessor |
5 | or using the regular getter syntax |
Fields
Private fields
Since traits allow the use of private methods, it can also be interesting to use private fields to store state. Traits will let you do that:
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)
This is a major difference with Java 8 virtual extension methods. While virtual extension methods do not carry state, traits can. Also interesting traits in Groovy are supported starting with Java 6, but their implementation do not rely on virtual extension methods. This means that even if a trait can be seen from a Java class as a regular interface, this interface will not have default methods, only abstract ones. |
Public fields
Public fields work the same way as private fields, but in order to avoid the diamond problem, field names are remapped in the implementing class:
trait Named {
public String name (1)
}
class Person implements Named {} (2)
def p = new Person() (3)
p.Named__name = 'Bob' (4)
1 | declare a public field inside the trait |
2 | declare a class implementing the trait |
3 | create an instance of that class |
4 | the public field is available, but renamed |
The name of the field depends on the fully qualified name of the trait. All dots (.
) in package are replaced with an underscore (_
), and the final name includes a double underscore.
So if the type of the field is String
, the name of the package is my.package
, the name of the trait is Foo
and the name of the field is bar
,
in the implementing class, the public field will appear as:
String my_package_Foo__bar
While traits support public fields, it is not recommanded to use them and considered as a bad practice. |
Composition of behaviors
Traits can be used to implement multiple inheritance in a controlled way, avoiding the diamond issue. For example, we can have the following traits:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
trait SpeakingAbility {
String speak() { "I'm speaking!" }
}
And a class implementing both traits:
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 | the Duck class implements both FlyingAbility and SpeakingAbility |
2 | creates a new instance of Duck |
3 | we can call the method fly from FlyingAbility |
4 | but also the method speak from SpeakingAbility |
Traits encourage the reuse of capabilities among objects, and the creation of new classes by the composition of existing behavior.
Overriding default methods
Traits provide default implementations for methods, but it is possible to override them in the implementing class. For example, we can slightly change the example above, by having a duck which quacks:
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 | define a method specific to Duck , named quack |
2 | override the default implementation of speak so that we use quack instead |
3 | the duck is still flying, from the default implementation |
4 | quack comes from the Duck class |
5 | speak no longer uses the default implementation from SpeakingAbility |
Extending traits
Simple inheritance
Traits may extend another trait, in which case you must use the extends
keyword:
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 | the Named trait defines a single name property |
2 | the Polite trait extends the Named trait |
3 | Polite adds a new method which has access to the name property of the super-trait |
4 | the name property is visible from the Person class implementing Polite |
5 | as is the introduce method |
Multiple inheritance
Alternatively, a trait may extend multiple traits. In that case, all super traits must be declared in the implements
clause:
trait WithId { (1)
Long id
}
trait WithName { (2)
String name
}
trait Identified implements WithId, WithName {} (3)
1 | WithId trait defines the id property |
2 | WithName trait defines the name property |
3 | Identified is a trait which inherits both WithId and WithName |
Duck typing and traits
Dynamic code
Traits can call any dynamic code, like a normal Groovy class. This means that you can, in the body of a method, call methods which are supposed to exist in an implementing class, without having to explicitly declare them in an interface. This means that traits are fully compatible with 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 | the SpeakingDuck expects the quack method to be defined |
2 | the Duck class does implement the method using methodMissing |
3 | calling the speak method triggers a call to quack which is handled by methodMissing |
Dynamic methods in a trait
It is also possible for a trait to implement MOP methods like methodMissing
or propertyMissing
, in which case implementing classes
will inherit the behavior from the trait, like in this example:
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 | create a trait implementing several MOP methods |
2 | the Dynamic class defines a property |
3 | the Dynamic class defines a method |
4 | calling an existing property will call the method from Dynamic |
5 | calling an non-existing property will call the method from the trait |
6 | will call setProperty defined on the trait |
7 | will call getProperty defined on the trait |
8 | calling an existing method on Dynamic |
9 | but calling a non existing method thanks to the trait methodMissing |
Multiple inheritance conflicts
Default conflict resolution
It is possible for a class to implement multiple traits. If some trait defines a method with the same signature as a method in another trait, we have a conflict:
trait A {
String exec() { 'A' } (1)
}
trait B {
String exec() { 'B' } (2)
}
class C implements A,B {} (3)
1 | trait A defines a method named exec returning a String |
2 | trait B defines the very same method |
3 | class C implements both traits |
In this case, the default behavior is that methods from the last declared trait wins. Here, B
is declared after A
so the method from B
will be picked up:
def c = new C()
assert c.exec() == 'B'
User conflict resolution
In case this behavior is not the one you want, you can explicitly choose which method to call using the Trait.super.foo
syntax.
In the example above, we can force to choose the method from trait A, by writing this:
class C implements A,B {
String exec() { A.super.exec() } (1)
}
def c = new C()
assert c.exec() == 'A' (2)
1 | explicit call of exec from the trait A |
2 | calls the version from A instead of using the default resolution, which would be the one from B |
Runtime implementation of traits
Implementing a trait at runtime
Groovy also supports implementing traits dynamically at runtime. It allows you to "decorate" an existing object using a trait. As an example, let’s start with this trait and the following class:
trait Extra {
String extra() { "I'm an extra method" } (1)
}
class Something { (2)
String doSomething() { 'Something' } (3)
}
1 | the Extra trait defines an extra method |
2 | the Something class does not implement the Extra trait |
3 | Something only defines a method doSomething |
Then if we do:
def s = new Something()
s.extra()
the call to extra would fail because Something
is not implementing Extra
. It is possible to do it at runtime with
the following syntax:
def s = new Something() as Extra (1)
s.extra() (2)
s.doSomething() (3)
1 | use of the as keyword to coerce an object to a trait at runtime |
2 | then extra can be called on the object |
3 | and doSomething is still callable |
When coercing an object to a trait, the result of the operation is not the same instance. It is guaranteed that the coerced object will implement both the trait and the interfaces that the original object implements, but the result will not be an instance of the original class. |
Implementing multiple traits at once
Should you need to implement several traits at once, you can use the withTraits
method instead of the as
keyword:
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 | call to methodFromA will fail because C doesn’t implement A |
2 | call to methodFromB will fail because C doesn’t implement B |
3 | withTrait will wrap c into something which implements A and B |
4 | methodFromA will now pass because d implements A |
5 | methodFromB will now pass because d also implements B |
When coercing an object to multiple traits, the result of the operation is not the same instance. It is guaranteed that the coerced object will implement both the traits and the interfaces that the original object implements, but the result will not be an instance of the original class. |
Chaining behavior
Groovy supports the concept of stackable traits. The idea is to delegate from one trait to the other if the current trait is not capable of handling a message. To illustrate this, let’s imagine a message handler interface like this:
interface MessageHandler {
void on(String message, Map payload)
}
Then you can compose a message handler by applying small behaviors. For example, let’s define a default handler in the form of a trait:
trait DefaultHandler implements MessageHandler {
void on(String message, Map payload) {
println "Received $message with payload $payload"
}
}
Then any class can inherit the behavior of the default handler by implementing the trait:
class SimpleHandler implements DefaultHandler {}
Now what if you want to log all messages, in addition to the default handler? One option is to write this:
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 | explicitly implement the on method |
2 | perform logging |
3 | continue by delegating to the DefaultHandler trait |
This works but this approach has drawbacks:
-
the logging logic is bound to a "concrete" handler
-
we have an explicit reference to
DefaultHandler
in theon
method, meaning that if we happen to change the trait that our class implements, code will be broken
As an alternative, we can write another trait which responsability is limited to logging:
trait LoggingHandler implements MessageHandler { (1)
void on(String message, Map payload) {
println "Seeing $message with payload $payload" (2)
super.on(message, payload) (3)
}
}
1 | the logging handler is itself a handler |
2 | prints the message it receives |
3 | then super makes it delegate the call to the next trait in the chain |
Then our class can be rewritten as this:
class HandlerWithLogger implements DefaultHandler, LoggingHandler {}
def loggingHandler = new HandlerWithLogger()
loggingHandler.on('test logging', [:])
which will print:
Seeing test logging with payload [:] Received test logging with payload [:]
As the priority rules imply that LoggerHandler
wins because it is declared last, then a call to on
will use
the implementation from LoggingHandler
. But the latter has a call to super
, which means the next trait in the
chain. Here, the next trait is DefaultHandler
so both will be called:
The interest of this approach becomes more evident if we add a third handler, which is responsible for handling messages
that start with 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 | a handler specific precondition |
2 | if the precondition is not meant, pass the message to the next handler in the chain |
Then our final handler looks like this:
class Handler implements DefaultHandler, SayHandler, LoggingHandler {}
def h = new Handler()
h.on('foo', [:])
h.on('sayHello', [:])
Which means:
-
messages will first go through the logging handler
-
the logging handler calls
super
which will delegate to the next handler, which is theSayHandler
-
if the message starts with
say
, then the hanlder consumes the message -
if not, the
say
handler delegates to the next handler in the chain
This approach is very powerful because it allows you to write handlers that do not know each other and yet let you combine them in the order you want. For example, if we execute the code, it will print:
Seeing foo with payload [:] Received foo with payload [:] Seeing sayHello with payload [:] I say Hello!
but if we move the logging handler to be the second one in the chain, the output is different:
class AlternateHandler implements DefaultHandler, LoggingHandler, SayHandler {}
h = new AlternateHandler()
h.on('foo', [:])
h.on('sayHello', [:])
prints:
Seeing foo with payload [:] Received foo with payload [:] I say Hello!
The reason is that now, since the SayHandler
consumes the message without calling super
, the logging handler is
not called anymore.
Semantics of super inside a trait
If a class implements multiple traits and that a call to an unqualified super
is found, then:
-
if the class implements another trait, the call delegates to the next trait in the chain
-
if there isn’t any trait left in the chain,
super
refers to the super class of the implementing class (this)
For example, it is possible to decorate final classes thanks to this behavior:
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 | define a trait named Filtering , supposed to be applied on a StringBuilder at runtime |
2 | redefine the append method |
3 | remove all 'o’s from the string |
4 | then delegate to super |
5 | in case toString is called, delegate to super.toString |
6 | runtime implementation of the Filtering trait on a StringBuilder instance |
7 | the string which has been appended no longer contains the letter o |
In this example, when super.append
is encountered, there is no other trait implemented by the target object, so the
method which is called is the original append
method, that is to say the one from StringBuilder
. The same trick
is used for toString
, so that the string representation of the proxy object which is generated delegates to the
toString
of the StringBuilder
instance.
Advanced features
SAM type coercion
If a trait defines a single abstract method, it is candidate for SAM type coercion. For example, imagine the following trait:
trait Greeter {
String greet() { "Hello $name" } (1)
abstract String getName() (2)
}
1 | the greet method is not abstract and calls the abstract method getName |
2 | getName is an abstract method |
Since getName
is the single abstract method in the Greeter
trait, you can write:
Greeter greeter = { 'Alice' } (1)
1 | the closure "becomes" the implementation of the getName single abstract method |
or even:
void greet(Greeter g) { println g.greet() } (1)
greet { 'Alice' } (2)
1 | the greet method accepts the SAM type Greeter as parameter |
2 | we can call it directly with a closure |
Differences with Java 8 default methods
In Java 8, interfaces can have default implementations of methods. If a class implements an interface and does not provide an implementation for a default method, then the implementation from the interface is chosen. Traits behave the same but with a major difference: the implementation from the trait is always used if the class declares the trait in its interface list and that it doesn’t provide an implementation.
This feature can be used to compose behaviors in an very precise way, in case you want to override the behavior of an already implemented method.
To illustrate the concept, let’s start with this simple example:
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() { /* ... */ }
}
In this example, we create a simple test case which uses two properties (config and shell) and uses those in
multiple test methods. Now imagine that you want to test the same, but with another distinct compiler configuration.
One option is to create a subclass of SomeTest
:
class AnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
It works, but what if you have actually multiple test classes, and that you want to test the new configuration for all those test classes? Then you would have to create a distinct subclass for each test class:
class YetAnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
Then what you see is that the setup
method of both tests is the same. The idea, then, is to create a trait:
trait MyTestSupport {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( new ASTTransformationCustomizer(CompileStatic) )
shell = new GroovyShell(config)
}
}
Then use it in the subclasses:
class AnotherTest extends SomeTest implements MyTestSupport {}
class YetAnotherTest extends SomeTest2 implements MyTestSupport {}
...
It would allow us to dramatically reduce the boilerplate code, and reduces the risk of forgetting to change the setup
code in case we decide to change it. Even if setup
is already implemented in the super class, since the test class declares
the trait in its interface list, the behavior will be borrowed from the trait implementation!
This feature is in particular useful when you don’t have access to the super class source code. It can be used to mock methods or force a particular implementation of a method in a subclass. It lets you refactor your code to keep the overriden logic in a single trait and inherit a new behavior just by implementing it. The alternative, of course, is to override the method in every place you would have used the new code.
It’s worth noting that if you use runtime traits, the methods from the trait are always preferred to those of the proxied object: |
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 | the Person class defines a name property which results in a getName method |
2 | Bob is a trait which defines getName as returning Bob |
3 | the default object will return Alice |
4 | p2 coerces p into Bob at runtime |
5 | getName returns Bob because getName is taken from the trait |
Again, don’t forget that dynamic trait coercion returns a distinct object which only implements the original interfaces, as well as the traits. |
Differences with mixins
There are several conceptual differences with mixins, as they are available in Groovy. Note that we are talking about runtime mixins, not the @Mixin annotation which is deprecated in favour of traits.
First of all, methods defined in a trait are visible in bytecode:
-
internally, the trait is represented as an interface (without default methods) and several helper classes
-
this means that an object implementing a trait effectively implements an interface
-
those methods are visible from Java
-
they are compatible with type checking and static compilation
Methods added through a mixin are, on the contrary, only visible at runtime:
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 | class A defines methodFromA |
2 | class B defines methodFromB |
3 | mixin B into A |
4 | we can call methodFromA |
5 | we can also call methodFromB |
6 | the object is an instance of A |
7 | but it’s not an instanceof B |
The last point is actually a very important and illustrates a place where mixins have an advantage over traits: the instances are not modified, so if you mixin some class into another, there isn’t a third class generated, and methods which respond to A will continue responding to A even if mixed in.
Static methods, properties and fields
The following instructions are subject to caution. Static member support is work in progress and still experimental. The information below is valid for 2.3.6 only. |
It is possible to define static methods in a trait, but it comes with numerous limitations:
-
traits with static methods cannot be compiled statically or type checked. All static methods/properties/field are accessed dynamically (it’s a limitation from the JVM).
-
the trait is interpreted as a template for the implementing class, which means that each implementing class will get its own static methods/properties/methods. So a static member declared on a trait doesn’t belong to the
Trait
, but to it’s implementing class.
Let’s start with a simple example:
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 | the static field is declared in the trait |
2 | a static method is also declared in the trait |
3 | the static field is updated within the trait |
4 | a static method init is made available to the implementing class |
5 | the static field is remapped to avoid the diamond issue |
As usual, it is not recommanded to use public fields. Anyway, should you want this, you must understand that the following code would fail:
Foo.CALLED = true
because there is no static field CALLED defined on the trait itself. Likewise, if you have two distinct implementing classes, each one gets a distinct static field:
class Bar implements TestHelper {} (1)
class Baz implements TestHelper {} (2)
Bar.init() (3)
assert Bar.TestHelper__CALLED (4)
assert !Baz.TestHelper__CALLED (5)
1 | class Bar implements the trait |
2 | class Baz also implements the trait |
3 | init is only called on Bar |
4 | the static field CALLED on Bar is updated |
5 | but the static field CALLED on Baz is not, because it is distinct |
Inheritance of state gotchas
We have seen that traits are stateful. It is possible for a trait to define fields or properties, but when a class implements a trait, it gets those fields/properties on a per-trait basis. So consider the following example:
trait IntCouple {
int x = 1
int y = 2
int sum() { x+y }
}
The trait defines two properties, x
and y
, as well as a sum
method. Now let’s create a class which implements the trait:
class BaseElem implements IntCouple {
int f() { sum() }
}
def base = new BaseElem()
assert base.f() == 3
The result of calling f
is 3
, because f
delegates to sum
in the trait, which has state. But what if we write this instead?
class Elem implements IntCouple {
int x = 3 (1)
int y = 4 (2)
int f() { sum() } (3)
}
def elem = new Elem()
1 | Override property x |
2 | Override property y |
3 | Call sum from trait |
If you call elem.f()
, what is the expected output? Actually it is:
assert elem.f() == 3
The reason is that the sum
method accesses the fields of the trait. So it is using the x
and y
values defined
in the trait. If you want to use the values from the implementing class, then you need to derefencence fields by using
getters and setters, like in this last example:
trait 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
Limitations
Compatibility with AST transformations
Traits are not officially compatible with AST transformations. Some of them, like @CompileStatic will be applied
on the trait itself (not on implementing classes), while others will apply on both the implementing class and the trait.
There is absolutely no guarantee that an AST transformation will run on a trait as it does on a regular class, so use it
at your own risk!
|
Prefix and postfix operations
Within traits, prefix and postfix operations are not allowed if they update a field of the trait:
trait Counting {
int x
void inc() {
x++ (1)
}
void dec() {
--x (2)
}
}
class Counter implements Counting {}
def c = new Counter()
c.inc()
1 | x is defined within the trait, postfix increment is not allowed |
2 | x is defined within the trait, prefix decrement is not allowed |
A workaround is to use the +=
operator instead.
:leveloffset: 0
1.4.3. Closures (TBD)
This chapter covers Groovy Closures.
Syntax (TBD)
Parameters (TBD)
Normal parameters (TBD)
Implicit parameter (TBD)
Default parameter (TBD)
Varargs (TBD)
Delegation strategy (TBD)
Closures in GStrings (TBD)
Functional programming (TBD)
Currying (TBD)
Memoization (TBD)
Composition (TBD)
Trampoline (TBD)
1.4.4. Semantics
This chapter covers the semantics of the Groovy programming language.
Statements
Variable definition
Variables can be defined using either their type (like String
) or by using the keyword def
:
String x
def o
def
is a replacement for a type name. In variable definitions it is used to indicate that you don’t care about the type. In variable definitions it is mandatory to either provide a type name explicitly or to use "def" in replacement. This is needed to the make variable definitions detectable for the Groovy parser.
You can think of def
as an alias of Object
and you will understand it in an instant.
Variable definition types can be refined by using generics, like in List<String> names .
To learn more about the generics support, please read the generics section.
|
Variable assignment
You can assign values to variables for later use. Try the following:
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 supports multiple assignment, i.e. where multiple variables can be assigned at once, e.g.:
def (a, b, c) = [10, 20, 'foo']
assert a == 10 && b == 20 && c == 'foo'
You can provide types as part of the declaration if you wish:
def (int i, String j) = [10, 'foo']
assert i == 10 && j == 'foo'
As well as used when declaring variables it also applies to existing variables:
def nums = [1, 3, 5]
def a, b, c
(a, b, c) = nums
assert a == 1 && b == 3 && c == 5
The syntax works for arrays as well as lists, as well as methods that return either of these:
def (_, month, year) = "18th June 2009".split()
assert "In $month of $year" == 'In June of 2009'
If the left hand side has too many variables, excess ones are filled with null’s:
def (a, b, c) = [1, 2]
assert a == 1 && b == 2 && c == null
If the right hand side has too many variables, the extra ones are ignored:
def (a, b) = [1, 2, 3]
assert a == 1 && b == 2
In the section describing the various Groovy operators,
the case of the subscript operator has been covered,
explaining how you can override the getAt()
/putAt()
method.
With this technique, we can combine multiple assignments and the subscript operator methods to implement object destructuring.
Consider the following immutable Coordinates
class, containing a pair of longitude and latitude doubles,
and notice our implementation of the getAt()
method:
@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")
}
}
Now let’s instantiate this class and destructure its longitude and latitude:
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 | we create an instance of the Coordinates class |
2 | then, we use a multiple assignment to get the individual longitude and latitude values |
3 | and we can finally assert their values. |
Control structures (WIP)
Groovy supports the usual if - else syntax from Java
def x = false
def y = false
if ( !x ) {
x = true
}
assert x == true
if ( x ) {
x = false
} else {
y = true
}
assert x == y
Groovy also supports the normal Java "nested" if then else if syntax:
if ( ... ) {
...
} else if (...) {
...
} else {
...
}
The switch statement in Groovy is backwards compatible with Java code; so you can fall through cases sharing the same code for multiple matches.
One difference though is that the Groovy switch statement can handle any kind of switch value and different kinds of matching can be performed.
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 supports the following kinds of comparisons:
-
Class case values matches if the switch value is an instance of the class
-
Regular expression case values match if the
toString()
representation of the switch value matches the regex -
Collection case values match if the switch value is contained in the collection. This also includes ranges (since they are Lists)
-
Closure case values match if the calling the closure returns a result which is true according to the Groovy truth
-
If none of the above are used then the case value matches if the case value equals the switch value
default must go at the end of the switch/case. While in Java the default can be placed anywhere in the switch/case, the default in Groovy is used more as an else than assigning a default case.
|
when using a closure case value, the default it parameter is actually the switch value (in our example, variable x )
|
Groovy supports the standard Java / C for loop:
String message = ''
for (int i = 0; i < 5; i++) {
message += 'Hi '
}
assert message == 'Hi Hi Hi Hi Hi '
The for loop in Groovy is much simpler and works with any kind of array, collection, Map, etc.
// 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 also supports the Java colon variation with colons: for (char c : text) {} ,
where the type of the variable is mandatory.
|
Groovy supports the usual while {…} loops like Java:
def x = 0
def y = 5
while ( y-- > 0 ) {
x++
}
assert x == 5
Exception handling is the same as Java.
You can specify a complete try-catch-finally
, a try-catch
, or a try-finally
set of blocks.
Braces are required around each block’s body. |
try {
'moo'.toLong() // this will generate an exception
assert false // asserting that this point should never be reached
} catch ( e ) {
assert e in NumberFormatException
}
We can put code within a finally clause following a matching try clause, so that regardless of whether the code in the try clause throws an exception, the code in the finally clause will always execute:
def z
try {
def i = 7, j = 0
try {
def k = i / j
assert false //never reached due to Exception in previous line
} finally {
z = 'reached here' //always executed even if Exception thrown
}
} catch ( e ) {
assert e in ArithmeticException
assert z == 'reached here'
}
With the multi catch block (since Groovy 2.0), we’re able to define several exceptions to be catch and treated by the same catch block:
try {
/* ... */
} catch ( IOException | NullPointerException e ) {
/* one block to handle 2 exceptions */
}
Power assertion (TBD)
Labeled statements (TBD)
Expressions (TBD)
GPath expressions (TBD)
Promotion and coercion (TBD)
Number promotion (TBD)
Closure to type coercion
A SAM type is a type which defines a single abstract method. This includes:
interface Predicate<T> {
boolean accept(T obj)
}
abstract class Greeter {
abstract String getName()
void greet() {
println "Hello, $name"
}
}
Any closure can be converted into a SAM type using the as
operator:
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
However, the as Type
expression is optional since Groovy 2.2.0. You can omit it and simply write:
Predicate filter = { it.contains 'G' }
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' }
greeter.greet()
which means you are also allowed to use method pointers, as shown in the following example:
boolean doFilter(String s) { s.contains('G') }
Predicate filter = this.&doFilter
assert filter.accept('Groovy') == true
Greeter greeter = GroovySystem.&getVersion
greeter.greet()
The second and probably more important use case for closure to SAM type coercion is calling a method which accepts a SAM type. Imagine the following method:
public <T> List<T> filter(List<T> source, Predicate<T> predicate) {
source.findAll { predicate.accept(it) }
}
Then you can call it with a closure, without having to create an explicit implementation of the interface:
assert filter(['Java','Groovy'], { it.contains 'G'} as Predicate) == ['Groovy']
But since Groovy 2.2.0, you are also able to omit the explicit coercion and call the method as if it used a closure:
assert filter(['Java','Groovy']) { it.contains 'G'} == ['Groovy']
As you can see, this has the advantage of letting you use the closure syntax for method calls, that is to say put the closure outside of the parenthesis, improving the readability of your code.
In addition to SAM types, a closure can be coerced to any type and in particular interfaces. Let’s define the following interface:
interface FooBar {
int foo()
void bar()
}
You can coerce a closure into the interface using the as
keyword:
def impl = { println 'ok'; 123 } as FooBar
This produces a class for which all methods are implemented using the closure:
assert impl.foo() == 123
impl.bar()
But it is also possible to coerce a closure to any class. For example, we can replace the interface
that we defined
with class
without changing the assertions:
class FooBar {
int foo() { 1 }
void bar() { println 'bar' }
}
def impl = { println 'ok'; 123 } as FooBar
assert impl.foo() == 123
impl.bar()
Map to type coercion
Usually using a single closure to implement an interface or a class with multiple methods is not the way to go. As an
alternative, Groovy allows you to coerce a map into an interface or a class. In that case, keys of the map are
interpreted as method names, while the values are the method implementation. The following example illustrates the
coercion of a map into an Iterator
:
def map
map = [
i: 10,
hasNext: { map.i > 0 },
next: { map.i-- },
]
def iter = map as Iterator
Of course this is a rather contrived example, but illustrates the concept. You only need to implement those methods
that are actually called, but if a method is called that doesn’t exist in the map a MissingMethodException
or an
UnsupportedOperationException
is thrown, depending on the arguments passed to the call,
as in the following example:
interface X {
void f()
void g(int n)
void h(String s, int n)
}
x = [ f: {println "f called"} ] as X
x.f() // method exists
x.g() // MissingMethodException here
x.g(5) // UnsupportedOperationException here
The type of the exception depends on the call itself:
-
MissingMethodException
if the arguments of the call do not match those from the interface/class -
UnsupportedOperationException
if the arguments of the call match one of the overloaded methods of the interface/class
String to enum coercion
Groovy allows transparent String
(or GString
) to enum values coercion. Imagine you define the following enum:
enum State {
up,
down
}
then you can assign a string to the enum without having to use an explicit as
coercion:
State st = 'up'
assert st == State.up
It is also possible to use a GString
as the value:
def val = "up"
State st = "${val}"
assert st == State.up
However, this would throw a runtime error (IllegalArgumentException
):
State st = 'not an enum value'
Note that it is also possible to use implicit coercion in switch statements:
State switchState(State st) {
switch (st) {
case 'up':
return State.down // explicit constant
case 'down':
return 'up' // implicit coercion for return types
}
}
in particular, see how the case
use string constants. But if you call a method that uses an enum with a String
argument, you still have to use an explicit as
coercion:
assert switchState('up' as State) == State.down
assert switchState(State.down) == State.up
Custom type coercion
It is possible for a class to define custom coercion strategies by implementing the asType
method. Custom coercion
is invoked using the as
operator and is never implicit. As an example,
imagine you defined two classes, Polar
and Cartesian
, like in the following example:
class Polar {
double r
double phi
}
class Cartesian {
double x
double y
}
And that you want to convert from polar coordinates to cartesian coordinates. One way of doing this is to define
the asType
method in the Polar
class:
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
which allows you to use the as
coercion operator:
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
Putting it all together, the Polar
class looks like this:
class Polar {
double r
double phi
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
}
but it is also possible to define asType
outside of the Polar
class, which can be practical if you want to define
custom coercion strategies for "closed" classes or classes for which you don’t own the source code, for example using
a metaclass:
Polar.metaClass.asType = { Class target ->
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
Class literals vs variables and the as operator
Using the as
keyword is only possible if you have a static reference to a class, like in the following code:
interface Greeter {
void greet()
}
def greeter = { println 'Hello, Groovy!' } as Greeter // Greeter is known statically
greeter.greet()
But what if you get the class by reflection, for example by calling Class.forName
?
Class clazz = Class.forName('Greeter')
Trying to use the reference to the class with the as
keyword would fail:
greeter = { println 'Hello, Groovy!' } as clazz
// throws:
// unable to resolve class clazz
// @ line 9, column 40.
// greeter = { println 'Hello, Groovy!' } as clazz
It is failing because the as
keyword only works with class literals. Instead, you need to call the asType
method:
greeter = { println 'Hello, Groovy!' }.asType(clazz)
greeter.greet()
Optionality (TBD)
Optional parentheses (TBD)
Optional semicolons (TBD)
Optional return keyword (TBD)
Optional public keyword (TBD)
The Groovy Truth (TBD)
Customizing the truth with asBoolean() methods (TBD)
Typing (WIP)
Optional typing
Optional typing is the idea that a program can work even if you don’t put an explicit type on a variable. Being a dynamic language, Groovy naturally implements that feature, for example when you declare a variable:
String aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | foo is declared using an explicit type, String |
2 | we can call the toUpperCase method on a String |
Groovy will let you write this instead:
def aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | foo is declared using def |
2 | we can still call the toUpperCase method, because the type of aString is resolved at runtime |
So it doesn’t matter that you use an explicit type here. It is in particular interesting when you combine this feature with static type checking, because the type checker performs type inference.
Likewise, Groovy doesn’t make it mandatory to declare the types of a parameter in a method:
String concat(String a, String b) {
a+b
}
assert concat('foo','bar') == 'foobar'
can be rewritten using def
as both return type and parameter types, in order to take advantage of duck typing, as
illustrated in this example:
def concat(def a, def b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | both the return type and the parameter types use def |
2 | it makes it possible to use the method with String |
3 | but also with int`s since the `plus method is defined |
Using the def keyword here is recommanded to describe the intent of a method which is supposed to work on any
type, but technically, we could use Object instead and the result would be the same: def is, in Groovy, strictly
equivalent to using Object .
|
Eventually, the type can be removed altogether from both the return type and the descriptor. But if you want to remove it from the return type, you then need to add an explicit modifier for the method, so that the compiler can make a difference between a method declaration and a method call, like illustrated in this example:
private concat(a,b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | if we want to omit the return type, an explicit modifier has to be set. |
2 | it is still possible to use the method with String |
3 | and also with `int`s |
Omitting types is in general considered a bad practice in method parameters or method return types for public APIs.
While using def in a local variable is not really a problem because the visibility of the variable is limited to the
method itself, while set on a method parameter, def will be converted to Object in the method signature, making it
difficult for users to know which is the expected type of the arguments. This means that you should limit this to cases
where you are explicitly relying on duck typing.
|
Static type checking
By default, Groovy performs minimal type checking at compile time. Since it is primarily a dynamic language, most checks that a static compiler would normally do aren’t possible at compile time. A method added via runtime metaprogramming might alter a class or object’s runtime behavior. Let’s illustrate why in the following example:
class Person { (1)
String firstName
String lastName
}
def p = new Person(firstName: 'Raymond', lastName: 'Devos') (2)
assert p.formattedName == 'Raymond Devos' (3)
1 | the Person class only defines two properties, firstName and lastName |
2 | we can create an instance of Person |
3 | and call a method named formattedName |
It is quite common in dynamic languages for code such as the above example not to throw any error. How can this be?
In Java, this would typically fail at compile time. However, in Groovy, it will not fail at compile time, and if coded
correctly, will also not fail at runtime. In fact, to make this work at runtime, one possibility is to rely on
runtime metaprogramming. So just adding this line after the declaration of the Person
class is enough:
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }
This means that in general, in Groovy, you can’t make any assumption about the type of an object beyond its declaration type, and even if you know it, you can’t determine at compile time what method will be called, or which property will be retrieved, and this is perfectly fine. This is how dynamic languages work, and it has a lot of interest.
However, if your program doesn’t rely on dynamic features and that you come from the static world (in particular, from
a Java mindset), not catching such "errors" at compile time can be surprising. As we have seen in the previous example,
the compiler cannot be sure this is an error. To make it aware that it is, you have to explicitly instruct the compiler
that you are switching to a type checked mode. This can be done by annotating a class or a method with @groovy.lang.TypeChecked
.
When type checking is activated, the compiler performs much more work:
-
type inference is activated, meaning that even if you use
def
on a local variable for example, the type checker will be able to infer the type of the variable from the assignments -
method calls are resolved at compile time, meaning that if a method is not declared on a class, the compiler will throw an error
-
in general, all the compile time errors that you are used to find in a static language will appear: method not found, property not found, incompatible types for method calls, number precision errors, …
In this section, we will describe the behavior of the type checker in various situations and explain the limits of using
@TypeChecked
on your code.
@TypeChecked
annotationThe groovy.lang.TypeChecked
annotation enabled type checking. It can be placed on a class:
@groovy.transform.TypeChecked
class Calculator {
int sum(int x, int y) { x+y }
}
Or on a method:
class Calculator {
@groovy.transform.TypeChecked
int sum(int x, int y) { x+y }
}
In the first case, all methods, properties, fields, inner classes, … of the annotated class will be type checked, whereas in the second case, only the method and potential closures or anonymous inner classes that it contains will be type checked.
The scope of type checking can be restricted. For example, if a class is type checked, you can instruct the type checker
to skip a method by annotating it with @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 | the GreetingService class is marked as type checked |
2 | so the greeting method is automatically type checked |
3 | but doGreet is marked with SKIP |
4 | the type checker doesn’t complain about missing properties here |
In the previous example, SentenceBuilder
relies on dynamic code. There’s no real Hello
method or property, so the
type checker would normally complain and compilation would fail. Since the method that uses the builder is marked with
TypeCheckingMode.SKIP
, type checking is skipped for this method, so the code will compile, even if the rest of the
class is type checked.
The following sections describe the semantics of type checking in Groovy.
An object o
of type A
can be assigned to a variable of type T
if and only if:
-
T
equalsA
Date now = new Date()
-
or
T
is one ofString
,boolean
,Boolean
orClass
String s = new Date() // implicit call to toString Boolean boxed = 'some string' // Groovy truth boolean prim = 'some string' // Groovy truth Class clazz = 'java.lang.String' // class coercion
-
or
o
is null andT
is not a primitive typeString s = null // passes int i = null // fails
-
or
T
is an array andA
is an array and the component type ofA
is assignable to the component type ofB
int[] i = new int[4] // passes int[] i = new String[4] // fails
-
or
T
is an array andA
is a list and the component type ofA
is assignable to the component type ofB
int[] i = [1,2,3] // passes int[] i = [1,2, new Date()] // fails
-
or
T
is a superclass ofA
AbstractList list = new ArrayList() // passes LinkedList list = new ArrayList() // fails
-
or
T
is an interface implemented byA
List list = new ArrayList() // passes RandomAccess list = new LinkedList() // fails
-
or
T
orA
are a primitive type and their boxed types are assignableint i = 0 Integer bi = 1 int x = new Integer(123) double d = new Float(5f)
-
or
T
extendsgroovy.lang.Closure
andA
is a SAM-type (single abstract method type)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
-
or
T
andA
derive fromjava.lang.Number
and conform to the following table
T | A | Examples |
---|---|---|
Double |
Any but BigDecimal or BigInteger |
|
Float |
Any type but BigDecimal, BigInteger or Double |
|
Long |
Any type but BigDecimal, BigInteger, Double or Float |
|
Integer |
Any type but BigDecimal, BigInteger, Double, Float or Long |
|
Short |
Any type but BigDecimal, BigInteger, Double, Float, Long or Integer |
|
Byte |
Byte |
|
In addition to the assignment rules above, if an assignment is deemed invalid, in type checked mode, a list literal or a map literal A
can be assigned
to a variable of type T
if:
-
the assignment is a variable declaration and
A
is a list literal andT
has a constructor whose parameters match the types of the elements in the list literal -
the assignment is a variable declaration and
A
is a map literal andT
has a no-arg constructor and a property for each of the map keys
For example, instead of writing:
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person classic = new Person('Ada','Lovelace')
You can use a "list constructor":
Person list = ['Ada','Lovelace']
or a "map constructor":
Person map = [firstName:'Ada', lastName:'Lovelace']
If you use a map constructor, additional checks are done on the keys of the map to check if a property of the same name is defined. For example, the following will fail at compile time:
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person map = [firstName:'Ada', lastName:'Lovelace', age: 24] (1)
1 | The type checker will throw an error No such property: age for class: Person at compile time |
In type checked mode, methods are resolved at compile time. Resolution works by name and arguments. The return type is irrelevant to method selection. Types of arguments are matched against the types of the parameters following those rules:
An argument o
of type A
can be used for a parameter of type T
if and only if:
-
T
equalsA
int sum(int x, int y) { x+y } assert sum(3,4) == 7
-
or
T
is aString
andA
is aGString
String format(String str) { "Result: $str" } assert format("${3+4}") == "Result: 7"
-
or
o
is null andT
is not a primitive typeString format(int value) { "Result: $value" } assert format(7) == "Result: 7" format(null) // fails
-
or
T
is an array andA
is an array and the component type ofA
is assignable to the component type ofB
String format(String[] values) { "Result: ${values.join(' ')}" } assert format(['a','b'] as String[]) == "Result: a b" format([1,2] as int[]) // fails
-
or
T
is a superclass ofA
String format(AbstractList list) { list.join(',') } format(new ArrayList()) // passes String format(LinkedList list) { list.join(',') } format(new ArrayList()) // fails
-
or
T
is an interface implemented byA
String format(List list) { list.join(',') } format(new ArrayList()) // passes String format(RandomAccess list) { 'foo' } format(new LinkedList()) // fails
-
or
T
orA
are a primitive type and their boxed types are assignableint 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
-
or
T
extendsgroovy.lang.Closure
andA
is a SAM-type (single abstract method type)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
-
or
T
andA
derive fromjava.lang.Number
and conform to the same rules as assignment of numbers
If a method with the appropriate name and arguments is not found at compile time, an error is thrown. The difference with "normal" Groovy is illustrated in the following example:
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine is an error, but since we’re in a dynamic mode, the error is not caught at compile time |
The example above shows a class that Groovy will be able to compile. However, if you try to create an instance of MyService
and call the
doSomething
method, then it will fail at runtime, because printLine
doesn’t exist. Of course, we already showed how Groovy could make
this a perfectly valid call, for example by catching MethodMissingException
or implementing a custom meta-class, but if you know you’re
not in such a case, @TypeChecked
comes handy:
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine is this time a compile-time error |
Just adding @TypeChecked
will trigger compile time method resolution. The type checker will try to find a method printLine
accepting
a String
on the MyService
class, but cannot find one. It will fail compilation with the following message:
Cannot find matching method MyService#printLine(java.lang.String)
It is important to understand the logic behind the type checker: it is a compile-time check, so by definition, the type checker
is not aware of any kind of runtime metaprogramming that you do. This means that code which is perfectly valid without @TypeChecked will
not compile anymore if you activate type checking. This is in particular true if you think of duck typing: |
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 | we define a Duck class which defines a quack method |
2 | we define another QuackingBird class which also defines a quack method |
3 | quacker is loosely typed, so since the method is @TypeChecked , we will obtain a compile-time error |
4 | even if in non type-checked Groovy, this would have passed |
There are possible workarounds, like introducing an interface, but basically, by activating type checking, you gain type safety but you loose some features of the language. Hopefully, Groovy introduces some features like flow typing to reduce the gap between type-checked and non type-checked Groovy.
When code is annotated with @TypeChecked
, the compiler performs type inference. It doesn’t simply rely on static types, but also uses various
techniques to infer the types of variables, return types, literals, … so that the code remains as clean as possible even if you activate the
type checker.
The simplest example is infering the type of a variable:
def message = 'Welcome to Groovy!' (1)
println message.toUpperCase() (2)
println message.upper() // compile time error (3)
1 | a variable is declared using the def keyword |
2 | calling toUpperCase is allowed by the type checker |
3 | calling upper will fail at compile time |
The reason the call to toUpperCase
works is because the type of message
was inferred as being a String
.
It is worth noting that although the compiler performs type inference on local variables, it does not perform any kind of type inference on fields, always falling back to the declared type of a field. To illustrate this, let’s take a look at this example:
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 uses def as a declaration type |
2 | someTypedField uses String as a declaration type |
3 | we can assign anything to someUntypedField |
4 | yet calling toUpperCase fails at compile time because the field is not typed properly |
5 | we can assign a String to a field of type String |
6 | and this time toUpperCase is allowed |
7 | if we assign a String to a local variable |
8 | then calling toUpperCase is allowed on the local variable |
Why such a difference? The reason is thread safety. At compile time, we can’t make any guarantee about the type of a field. Any thread can access any field at any time and between the moment a field is assigned a variable of some type in a method and the time is is used the line after, another thread may have changed the contents of the field. This is not the case for local variables: we know if they "escape" or not, so we can make sure that the type of a variable is constant (or not) over time. Note that even if a field is final, the JVM makes no guarantee about it, so the type checker doesn’t behave differently if a field is final or not.
This is one of the reasons why we recommend to use typed fields. While using def for local variables is perfectly
fine thanks to type inference, this is not the case for fields, which also belong to the public API of a class, hence the
type is important.
|
Groovy provides a syntax for various type literals. There are three native collection literals in Groovy:
-
lists, using the
[]
literal -
maps, using the
[:]
literal -
ranges, using the
(..,..)
literal
The inferred type of a literal depends on the elements of the literal, as illustrated in the following table:
Literal | Inferred type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
As you can see, with the noticeable exception of the IntRange
, the inferred type makes use of generics types to describe
the contents of a collection. In case the collection contains elements of different types, the type checker still performs
type inference of the components, but uses the notion of least upper bound.
In Groovy, the least upper bound of two types A
and B
is defined as a type which:
-
superclass corresponds to the common super class of
A
andB
-
interfaces correspond to the interfaces implemented by both
A
andB
-
if
A
orB
is a primitive type and thatA
isn’t equal toB
, the least upper bound ofA
andB
is the least upper bound of their wrapper types
If A
and B
only have one (1) interface in common and that their common superclass is Object
, then the LUB of both
is the common interface.
The least upper bound represents the minimal type to which both A
and B
can be assigned. So for example, if A
and B
are both String
, then the LUB (least upper bound) of both is also 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 | the LUB of String and String is String |
2 | the LUB of ArrayList and LinkedList is their common super type, AbstractList |
3 | the LUB of ArrayList and List is their only common interface, List |
4 | the LUB of two identical interfaces is the interface itself |
5 | the LUB of Bottom1 and Bottom2 is their superclass Top |
6 | the LUB of two types which have nothing in common is Object |
In those examples, the LUB is always representable as a normal, JVM supported, type. But Groovy internally represents the LUB as a type which can be more complex, and that you wouldn’t be able to use to define a variable for example. To illustrate this, let’s continue with this example:
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}
What is the least upper bound of Bottom
and SerializableFooImpl
? They don’t have a common super class (apart from Object
),
but they do share 2 interfaces (Serializable
and Foo
), so their least upper bound is a type which represents the union of
two interfaces (Serializable
and Foo
). This type cannot be defined in the source code, yet Groovy knows about it.
In the context of collection type inference (and generic type inference in general), this becomes handy, because the type of the components is inferred as the least upper bound. We can illustrate why this is important in the following example:
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 | the Greeter interface defines a single method, greet |
2 | the Salute interface defines a single method, salute |
3 | class A implements both Greeter and Salute but there’s no explicit interface extending both |
4 | same for B |
5 | but B defines an additional exit method |
6 | the type of list is inferred as "list of the LUB of A and B " |
7 | so it is possible to call greet which is defined on both A and B through the Greeter interface |
8 | and it is possible to call salut which is defined on both A and B through the Salut interface |
9 | yet calling exit is a compile time error because it doesn’t belong to the LUB of A and B (only defined in B ) |
The error message will look like:
[Static type checking] - Cannot find matching method Greeter or Salute#exit()
which indicates that the exit
method is neither defines on Greeter
nor Salute
, which are the two interfaces defined
in the least upper bound of A
and B
.
In normal, non type checked, Groovy, you can write things like:
class Greeter {
String greeting() { 'Hello' }
}
void doSomething(def o) {
if (o instanceof Greeter) { (1)
println o.greeting() (2)
}
}
doSomething(new Greeter())
1 | guard the method call with an instanceof check |
2 | make the call |
The method call works because of dynamic dispatch (the method is selected at runtime). The equivalent code in Java would
require to cast o
to a Greeter
before calling the greeting
method, because methods are selected at compile time:
if (o instanceof Greeter) {
System.out.println(((Greeter)o).greeting());
}
However, in Groovy, even if you add @TypeChecked
(and thus activate type checking) on the doSomething
method, the
cast is not necessary. The compiler embeds instanceof inference that makes the cast optional.
Flow typing is an important concept of Groovy in type checked mode and an extension of type inference. The idea is that the compiler is capable of infering the type of variables in the flow of the code, not just at initialization:
@groovy.transform.TypeChecked
void flowTyping() {
def o = 'foo' (1)
o = o.toUpperCase() (2)
o = 9d (3)
o = Math.sqrt(o) (4)
}
1 | first, o is declared using def and assigned a String |
2 | the compiler inferred that o is a String , so calling toUpperCase is allowed |
3 | o is reassigned with a double |
4 | calling Math.sqrt passes compilation because the compiler knows that at this point, o is a double |
So the type checker is aware of the fact that the concrete type of a variable is different over time. In particular, if you replace the last assignment with:
o = 9d
o = o.toUpperCase()
The type checker will now fail at compile time, because it knows that o
is a double
when toUpperCase
is called,
so it’s a type error.
It is important to understand that it is not the fact of declaring a variable with def
that triggers type inference.
Flow typing works for any variable of any type. Declaring a variable with an explicit type only constraints what you
can assign to a variable:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list = list*.toUpperCase() (2)
list = 'foo' (3)
}
1 | list is declared as an unchecked List and assigned a list literal of `String`s |
2 | this line passes compilation because of flow typing: the type checker knows that list is at this point a List<String> |
3 | but you can’t assign a String to a List so this is a type checking error |
You can also note that even if the variable is declared without generics information, the type checker knows what is the component type. Therefore, such code would fail compilation:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list.add(1) (2)
}
1 | list is inferred as List<String> |
2 | so adding an int to a List<String> is a compile-time error |
Fixing this requires adding an explicit generic type to the declaration:
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List<? extends Serializable> list = [] (1)
list.addAll(['a','b','c']) (2)
list.add(1) (3)
}
1 | list declared as List<? extends Serializable> and initialized with an empty list |
2 | elements added to the list conform to the declaration type of the list |
3 | so adding an int to a List<? extends Serializable> is allowed |
Flow typing has been introduced to reduce the difference in semantics between classic and static Groovy. In particular, consider the behavior of this code in 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 is declared as an Object and assigned a String |
2 | we call the compute method with o |
3 | and print the result |
In Java, this code will output 0
, because method selection is done at compile time and based on the declared types.
So even if o
is a String
at runtime, it is still the Object
version which is called, because o
has been declared
as an Object
. To be short, in Java, declared types are most important, be it variable types, parameter types or return
types.
In Groovy, we could write:
int compute(String string) { string.length() }
String compute(Object o) { "Nope" }
Object o = 'string'
def result = compute(o)
println result
But this time, it will return 6
, because the method which is chosen is chosen at runtime, based on the actual
argument types. So at runtime, o
is a String
so the String
variant is used. Note that this behavior has nothing
to do with type checking, it’s the way Groovy works in general: dynamic dispatch.
In type checked Groovy, we want to make sure the type checker selects the same method at compile time, that the runtime
would choose. It is not possible in general, due to the semantics of the language, but we can make things better with flow
typing. With flow typing, o
is inferred as a String
when the compute
method is called, so the version which takes
a String
and returns an int
is chosen. This means that we can infer the return type of the method to be an int
, and
not a String
. This is important for subsequent calls and type safety.
So in type checked Groovy, flow typing is a very important concept, which also implies that if @TypeChecked
is applied,
methods are selected based on the inferred types of the arguments, not on the declared types. This doesn’t ensure 100%
type safety, because the type checker may select a wrong method, but it ensures the closest semantics to dynamic Groovy.
A combination of flow typing and least upper bound inference is used to perform advanced type inference and ensure type safety in multiple situations. In particular, program control structures are likely to alter the inferred type of a variable:
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 | if someCondition is true, o is assigned a Top |
2 | if someCondition is false, o is assigned a Bottom |
3 | calling methodFromTop is safe |
4 | but calling methodFromBottom is not, so it’s a compile time error |
When the type checker visits an if/else
control structure, it checks all variables which are assigned in if/else
branches
and computes the least upper bound of all assignments. This type is the type of the inferred variable
after the if/else
block, so in this example, o
is assigned a Top
in the if
branch and a Bottom
in the else
branch. The LUB of those is a Top
, so after the conditional branches, the compiler infers o
as being
a Top
. Calling methodFromTop
will therefore be allowed, but not methodFromBottom
.
The same reasoning exists with closures and in particular closure shared variables. A closure shared variable is a variable which is defined outside of a closure, but used inside a closure, as in this example:
def text = 'Hello, world!' (1)
def closure = {
println text (2)
}
1 | a variable named text is declared |
2 | text is used from inside a closure. It is a closure shared variable. |
Groovy allows developers to use those variables without requiring them to be final. This means that a closure shared variable can be reassigned inside a closure:
String result
doSomething { String it ->
result = "Result: $it"
}
result = result?.toUpperCase()
The problem is that a closure is an independent block of code that can be executed (or not) at any time. In particular,
doSomething
may be asynchronous, for example. This means that the body of a closure doesn’t belong to the main control
flow. For that reason, the type checker also computes, for each closure shared variable, the LUB of all
assignments of the variable, and will use that LUB
as the inferred type outside of the scope of the closure, like in
this example:
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 | a closure-shared variable is first assigned a Top |
2 | inside the closure, it is assigned a Bottom |
3 | methodFromTop is allowed |
4 | methodFromBottom is a compilation error |
Here, it is clear that when methodFromBottom
is called, there’s no guarantee, at compile-time or runtime that the
type of o
will effectively be a Bottom
. There are chances that it will be, but we can’t make sure, because it’s
asynchronous. So the type checker will only allow calls on the least upper bound, which is here a Top
.
The type checker performs special inference on closures, resulting on additional checks on one side and improved fluency on the other side.
The first thing that the type checker is capable of doing is infering the return type of a closure. This is simply illustrated in the following example:
@groovy.transform.TypeChecked
int testClosureReturnTypeInference(String arg) {
def cl = { "Arg: $arg" } (1)
def val = cl() (2)
val.length() (3)
}
1 | a closure is defined, and it returns a string (more precisely a GString ) |
2 | we call the closure and assign the result to a variable |
3 | the type checker inferred that the closure would return a string, so calling length() is allowed |
As you can see, unlike a method which declares its return type explicitly, there’s no need to declare the return type of a closure: its type is inferred from the body of the closure.
In addition to the return type, it is possible for a closure to infer its parameter types from the context. There are two ways for the compiler to infer the parameter types:
-
through implicit SAM type coercion
-
through API metadata
To illustrate this, lets start with an example that will fail compilation due to the inability for the type checker to infer the parameter types:
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 | the inviteIf method accepts a Person and a Closure |
2 | we call it with a Person and a Closure |
3 | yet it is not statically known as being a Person and compilation fails |
In this example, the closure body contains it.age
. With dynamic, not type checked code, this would work, because the
type of it
would be a Person
at runtime. Unfortunately, at compile-time, there’s no way to know what is the type
of it
, just by reading the signature of inviteIf
.
To be short, the type checker doesn’t have enough contextual information on the inviteIf
method to determine statically
the type of it
. This means that the method call needs to be rewritten like this:
inviteIf(p) { Person it -> (1)
it.age >= 18
}
1 | the type of it needs to be declared explicitly |
By explicitly declaring the type of the it
variable, you can workaround the problem and make this code statically
checked.
For an API or framework designer, there are two ways to make this more elegant for users, so that they don’t have to declare an explicit type for the closure parameters. The first one, and easiest, is to replace the closure with a SAM type:
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 | declare a SAM interface with an apply method |
2 | inviteIf now uses a Predicate<Person> instead of a Closure<Boolean> |
3 | there’s no need to declare the type of the it variable anymore |
4 | it.age compiles properly, the type of it is inferred from the Predicate#apply method signature |
By using this technique, we leverage the automatic coercion of closures to SAM types feature of Groovy. The question whether you should use a SAM type or a Closure really depends on what you need to do. In a lot of cases, using a SAM interface is enough, especially if you consider functional interfaces as they are found in Java 8. However, closures provide features that are not accessible to functional interfaces. In particular, closures can have a delegate, and owner and can be manipulated as objects (for example, cloned, serialized, curried, …) before being called. They can also support multiple signatures (polymorphism). So if you need that kind of manipulation, it is preferable to switch to the most advanced type inference annotations which are described below. |
The original issue that needs to be solved when it comes to closure parameter type inference, that is to say, statically determining the types of the arguments of a closure without having to have them explicitly declared, is that the Groovy type system inherits the Java type system, which is insufficient to describe the types of the arguments.
@ClosureParams
annotationGroovy provides an annotation, @ClosureParams
which is aimed at completing type information. This annotation is primarily
aimed at framework and API developers who want to extend the capabilities of the type checker by providing type inference
metadata. This is important if your library makes use of closures and that you want the maximum level of tooling support
too.
Let’s illustrate this by fixing the original example, introducing the @ClosureParams
annotation:
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 | the closure parameter is annotated with @ClosureParams |
2 | it’s not necessary to use an explicit type for it , which is inferred |
The @ClosureParams
annotation minimally accepts one argument, which is named a type hint. A type hint is a class which
is reponsible for completing type information at compile time for the closure. In this example, the type hint being used
is groovy.transform.stc.FirstParam
which indicated to the type checker that the closure will accept one parameter
whose type is the type of the first parameter of the method. In this case, the first parameter of the method is Person
,
so it indicates to the type checker that the first parameter of the closure is in fact a Person
.
The second argument is optional and named options. It’s semantics depends on the type hint class. Groovy comes with various bundled type hints, illustrated in the table below:
Type hint | Polymorphic? | Description and examples |
---|---|---|
|
No |
The first (resp. second, third) parameter type of the method
|
|
No |
The first generic type of the first (resp. second, third) parameter of the method
Variants for |
|
No |
A type hint for which the type of closure parameters comes from the options string.
This type hint supports a single signature and each of the parameter is specified as a value of the options array using a fully-qualified type name or a primitive type. |
|
Yes |
A dedicated type hint for closures that either work on a
This type hint requires that the first argument is a |
|
Yes |
Infers closure parameter types from the abstract method of some type. A signature is inferred for each abstract method.
If there are multiple signatures like in the example above, the type checker will only be able to infer the types of
the arguments if the arity of each method is different. In the example above, |
|
Yes |
Infers the closure parameter typs from the A single signature for a closure accepting a
A polymorphic closure, accepting either a
A polymorphic closure, accepting either a
|
Even though you use FirstParam , SecondParam or ThirdParam as a type hint, it doesn’t stricly mean that the
argument which will be passed to the closure will be the first (resp. second, third) argument of the method call. It
only means that the type of the parameter of the closure will be the same as the type of the first (resp. second,
third) argument of the method call.
|
In short, the lack of the @ClosureParams
annotation on a method accepting a Closure
will not fail compilation. If
present (and it can be present in Java sources as well as Groovy sources), then the type checker has more information
and can perform additional type inference. This makes this feature particularily interesting for framework developers.
@DelegatesTo
The @DelegatesTo
annotation is used by the type checker to infer the type of the delegate. It allows the API designer
to instruct the compiler what is the type of the delegate and the delegation strategy. The @DelegatesTo
annotation is
discussed in a specific section.
Static compilation (WIP)
In the type checking section, we have seen that Groovy provides optional type checking thanks to the
@TypeChecked
annotation. The type checker runs at compile time and performs a static analysis of dynamic code. The
program will behave exactly the same whether type checking has been enabled or not. This means that the @TypeChecked
annotation is neutral with regards to the semantics of a program. Even though it may be necessary to add type information
in the sources so that the program is considered type safe, in the end, the semantics of the program are the same.
While this may sound fine, there is actually one issue with this: type checking of dynamic code, done at compile time, is by definition only correct if no runtime specific behavior occurs. For example, the following program passes type checking:
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'
}
}
There are two compute
methods. One accepts a String
and returns an int
, the other accepts an int
and returns
a String
. If you compile this, it is considered type safe: the inner compute('foobar')
call will return an int
,
and calling compute
on this int
will in turn return a String
.
Now, before calling test()
, consider adding the following line:
Computer.metaClass.compute = { String str -> new Date() }
Using runtime metaprogramming, we’re actually modifying the behavior of the compute(String)
method, so that instead of
returning the length of the provided argument, it will return a Date
. If you execute the program, it will fail at
runtime. Since this line can be added from anywhere, in any thread, there’s absolutely no way for the type checker to
statically make sure that no such thing happens. In short, the type checker is vulnerable to monkey patching. This is
just one example, but this illustrates the concept that doing static analysis of a dynamic program is inherently wrong.
The Groovy language provides an alternative annotation to @TypeChecked
which will actually make sure that the methods
which are inferred as being called will effectively be called at runtime. This annotation turns the Groovy compiler
into a static compiler, where all method calls are resolved at compile time and the generated bytecode makes sure
that this happens: the annotation is @groovy.transform.CompileStatic
.
@CompileStatic
annotationThe @CompileStatic
annotation can be added anywhere the @TypeChecked
annotation can be used, that is to say on
a class or a method. It is not necessary to add both @TypeChecked
and @CompileStatic
, as @CompileStatic
performs
everything @TypeChecked
does, but in addition triggers static compilation.
Let’s take the example which failed, but this time let’s replace the @TypeChecked
annotation
with @CompileStatic
:
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() }
run()
This is the only difference. If we execute this program, this time, there is no runtime error. The test
method
became immune to monkey patching, because the compute
methods which are called in its body are linked at compile
time, so even if the metaclass of Computer
changes, the program still behaves as expected by the type checker.
There are several benefits of using @CompileStatic
on your code:
-
type safety
-
immunity to monkey patching
-
performance improvements
The performance improvements depend on the kind of program you are executing. It it is I/O bound, the difference between statically compiled code and dynamic code is barely noticeable. On highly CPU intensive code, since the bytecode which is generated is very close, if not equal, to the one that Java would produce for an equivalent program, the performance is greatly improved.
Using the invokedynamic version of Groovy, which is accessible to people using JDK 7 and above, the performance of the dynamic code should be very close to the performance of statically compiled code. Sometimes, it can even be faster! There is only one way to determine which version you should choose: measuring. The reason is that depending on your program and the JVM that you use, the performance can be significantly different. In particular, the invokedynamic version of Groovy is very sensitive to the JVM version in use. |