本章介绍 Groovy 编程语言的语法。该语言的语法源于 Java 语法,但在 Java 语法基础上添加了 Groovy 特定的结构,并允许进行某些简化。
1. 注释
1.1. 单行注释
单行注释以 //
开头,可以出现在行中的任何位置。//
之后的字符(直到行尾)都被视为注释的一部分。
// a standalone single line comment
println "hello" // a comment till the end of the line
1.2. 多行注释
多行注释以 /*
开头,可以出现在行中的任何位置。/*
之后的字符(包括换行符)直到第一个 */
(关闭注释)都被视为注释的一部分。因此,多行注释可以放在语句末尾,甚至放在语句内部。
/* a standalone multiline comment
spanning two lines */
println "hello" /* a multiline comment starting
at the end of a statement */
println 1 /* one */ + 2 /* two */
1.3. Groovydoc 注释
与多行注释类似,Groovydoc 注释也是多行的,但以 /**
开头,以 */
结尾。第一个 Groovydoc 注释行后面的行可以(可选)以星号 *
开头。这些注释与以下语言元素相关联:
-
类型定义(类、接口、枚举、注解),
-
字段和属性定义
-
方法定义
虽然编译器不会对与上述语言元素无关的 Groovydoc 注释发出警告,但你应该在这些结构之前添加注释,紧挨着它。
/**
* A Class description
*/
class Person {
/** the name of the person */
String name
/**
* Creates a greeting method for a certain person.
*
* @param otherPerson the person to greet
* @return a greeting message
*/
String greet(String otherPerson) {
"Hello ${otherPerson}"
}
}
Groovydoc 遵循与 Java 的 Javadoc 相同的约定。因此,你可以使用与 Javadoc 相同的标签。
此外,从 3.0.0 版本开始,Groovy 支持**运行时 Groovydoc**,即 Groovydoc 可以保留在运行时。
默认情况下,运行时 Groovydoc 处于禁用状态。可以通过添加 JVM 选项 -Dgroovy.attach.runtime.groovydoc=true 来启用它。 |
运行时 Groovydoc 以 /**@
开头,以 */
结尾,例如:
/**@
* Some class groovydoc for Foo
*/
class Foo {
/**@
* Some method groovydoc for bar
*/
void bar() {
}
}
assert Foo.class.groovydoc.content.contains('Some class groovydoc for Foo') (1)
assert Foo.class.getMethod('bar', new Class[0]).groovydoc.content.contains('Some method groovydoc for bar') (2)
1 | 获取类 Foo 的运行时 groovydoc |
2 | 获取方法 bar 的运行时 groovydoc |
1.4. Shebang 行
除了单行注释外,还有一种特殊的行注释,通常被称为shebang 行,它被 UNIX 系统理解,允许脚本直接从命令行运行,前提是你已安装 Groovy 发行版,并且 groovy
命令在 PATH
上可用。
#!/usr/bin/env groovy
println "Hello from the shebang line"
# 字符必须是文件的第一个字符。任何缩进都会导致编译错误。 |
2. 关键字
Groovy 具有以下保留关键字
abstract |
assert |
break |
case |
catch |
class |
const |
continue |
def |
default |
do |
else |
enum |
extends |
final |
finally |
for |
goto |
if |
implements |
import |
instanceof |
interface |
native |
new |
null |
non-sealed |
package |
public |
protected |
private |
return |
static |
strictfp |
super |
switch |
synchronized |
this |
threadsafe |
throw |
throws |
transient |
try |
while |
其中,const
、goto
、strictfp
和 threadsafe
目前未被使用。
通常,保留关键字不能用作变量、字段和方法名。
此外,Groovy 还有以下上下文关键字
as |
in |
permits |
record |
sealed |
trait |
var |
yields |
这些词语仅在特定上下文中才是关键字,并且在某些地方可以使用得更加自由,特别是对于变量、字段和方法名。
对保留关键字的限制也适用于基本类型、布尔值字面量和空值字面量(所有这些将在后面讨论)。
null |
true |
false |
boolean |
char |
byte |
short |
int |
long |
float |
double |
3. 标识符
3.1. 普通标识符
标识符以字母、美元符号或下划线开头。它们不能以数字开头。
字母可以在以下范围中:
-
'a' 到 'z'(小写 ASCII 字母)
-
'A' 到 'Z'(大写 ASCII 字母)
-
'\u00C0' 到 '\u00D6'
-
'\u00D8' 到 '\u00F6'
-
'\u00F8' 到 '\u00FF'
-
'\u0100' 到 '\uFFFE'
然后后面的字符可以包含字母和数字。
以下是一些有效标识符的示例(这里是指变量名)
def name
def item3
def with_underscore
def $dollarStart
但以下标识符无效
def 3tier
def a+b
def a#b
所有关键字在点后面也是有效的标识符
foo.as
foo.assert
foo.break
foo.case
foo.catch
3.2. 引号标识符
引号标识符出现在点分表达式后面的点之后。例如,person.name
表达式中的 name
部分可以用 person."name"
或 person.'name'
来引用。这在某些标识符包含 Java 语言规范禁止的非法字符,但 Groovy 允许使用引号引用时特别有用。例如,连字符、空格、感叹号等字符。
def map = [:]
map."an identifier with a space and double quotes" = "ALLOWED"
map.'with-dash-signs-and-single-quotes' = "ALLOWED"
assert map."an identifier with a space and double quotes" == "ALLOWED"
assert map.'with-dash-signs-and-single-quotes' == "ALLOWED"
正如我们将在后面的字符串部分中看到的那样,Groovy 提供了不同类型的字符串字面量。实际上,所有类型的字符串都可以在点后面使用
map.'single quote'
map."double quote"
map.'''triple single quote'''
map."""triple double quote"""
map./slashy string/
map.$/dollar slashy string/$
普通字符字符串和 Groovy 的 GString(插值字符串)之间存在区别,因为在后者情况下,插值的值将插入最终字符串中以评估整个标识符
def firstname = "Homer"
map."Simpson-${firstname}" = "Homer Simpson"
assert map.'Simpson-Homer' == "Homer Simpson"
4. 字符串
文本字面量以字符链的形式表示,称为字符串。Groovy 允许你实例化 java.lang.String
对象,以及 GString(groovy.lang.GString
),它们在其他编程语言中也被称为插值字符串。
4.3. 三单引号字符串
三单引号字符串是用三个单引号括起来的字符序列
'''a triple-single-quoted string'''
三单引号字符串是普通的 java.lang.String ,不支持插值。 |
三单引号字符串可以跨越多行。字符串的内容可以跨越行边界,无需将字符串拆分成多个片段,也无需使用连接或换行转义字符。
def aMultilineString = '''line one
line two
line three'''
如果你的代码有缩进(例如,在类的某个方法的正文中),你的字符串将包含缩进的空格。Groovy 开发工具包包含用于去除缩进的方法,例如 String#stripIndent()
方法,以及 String#stripMargin()
方法,它接受一个分隔符字符以标识要从字符串开头删除的文本。
当以以下方式创建字符串时
def startingAndEndingWithANewline = '''
line one
line two
line three
'''
你会注意到,结果字符串的第一个字符是一个换行符。可以通过使用反斜杠转义换行符来去除该字符
def strippedFirstNewline = '''\
line one
line two
line three
'''
assert !strippedFirstNewline.startsWith('\n')
4.3.1. 转义特殊字符
可以使用反斜杠字符转义单引号,以避免终止字符串字面量
'an escaped single quote: \' needs a backslash'
还可以使用双反斜杠转义转义字符本身
'an escaped escape character: \\ needs a double backslash'
某些特殊字符也使用反斜杠作为转义字符
转义序列 | 字符 |
---|---|
\b |
退格符 |
\f |
换页符 |
\n |
换行符 |
\r |
回车符 |
\s |
单个空格 |
\t |
制表符 |
\\ |
反斜杠 |
\' |
单引号字符串中的单引号(在三单引号字符串和双引号字符串中可选) |
\" |
双引号字符串中的双引号(在三双引号字符串和单引号字符串中可选) |
在讨论其他类型的字符串时,我们将看到更多转义细节。
4.3.2. Unicode 转义序列
对于键盘上没有的字符,可以使用 Unicode 转义序列:一个反斜杠,后面跟着 'u',然后是 4 个十六进制数字。
例如,欧元货币符号可以用以下表示
'The Euro currency symbol: \u20AC'
4.4. 双引号字符串
双引号字符串是一系列用双引号括起来的字符。
"a double-quoted string"
如果双引号字符串中没有插值表达式,则它们是普通的java.lang.String ,但如果存在插值,则它们是groovy.lang.GString 实例。 |
要转义双引号,可以使用反斜杠字符:“双引号:\"”。 |
4.4.1. 字符串插值
除了单引号和三单引号字符串之外,所有字符串文字都可以插值任何 Groovy 表达式。插值是在字符串求值时用其值替换字符串中的占位符的行为。占位符表达式用${}
括起来。对于明确的点分表达式,可以省略花括号,即在这些情况下可以使用 $ 前缀。如果 GString 曾经传递给采用 String 的方法,则占位符内的表达式值将被计算为其字符串表示形式(通过对该表达式调用toString()
),并且结果 String 将传递给该方法。
这里,我们有一个字符串,其中一个占位符引用了一个局部变量
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 。因此,如果在该占位符中插入多个语句,则最后一个语句应该以某种方式返回一个有意义的值以供插入。例如,“1 和 2 的总和等于 ${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}()" 。 |
类似地,如果表达式不明确,则需要保留花括号
String thing = 'treasure'
assert 'The x-coordinate of the treasure is represented by treasure.x' ==
"The x-coordinate of the $thing is represented by $thing.x" // <= Not allowed: ambiguous!!
assert 'The x-coordinate of the treasure is represented by treasure.x' ==
"The x-coordinate of the $thing is represented by ${thing}.x" // <= Curly braces required
如果你需要在 GString 中转义$
或${}
占位符,使其原样出现而不进行插值,你只需要使用\
反斜杠字符转义美元符号
assert '$5' == "\$5"
assert '${name}' == "\${name}"
4.4.2. 插值闭包表达式的特殊情况
到目前为止,我们已经看到可以在${}
占位符内插值任意表达式,但对于闭包表达式,有一个特殊情况和符号。当占位符包含一个箭头时,${→}
,表达式实际上是一个闭包表达式——你可以将其视为一个闭包,前面有一个美元符号
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 时被调用,从而导致一个包含新数字值的更新后的字符串。 |
一个嵌入的闭包表达式接受多个参数将在运行时生成一个异常。只允许零参数或一个参数的闭包。 |
4.4.3. 与 Java 的互操作性
当一个方法(无论是在 Java 中实现还是在 Groovy 中实现)期望一个java.lang.String
,但我们传递一个groovy.lang.GString
实例时,会自动且透明地调用 GString 的toString()
方法。
String takeString(String message) { (4)
assert message instanceof String (5)
return message
}
def message = "The message is ${'hello'}" (1)
assert message instanceof GString (2)
def result = takeString(message) (3)
assert result instanceof String
assert result == 'The message is hello'
1 | 我们创建一个 GString 变量 |
2 | 我们再次检查它是否是 GString 的实例 |
3 | 然后我们将该 GString 传递给一个方法,该方法将 String 作为参数 |
4 | takeString() 方法的签名明确说明它的唯一参数是一个 String |
5 | 我们还验证该参数确实是一个 String,而不是一个 GString。 |
4.4.4. GString 和 String 哈希码
尽管插值字符串可以替代普通 Java 字符串,但它们在哈希码方面与字符串有所不同。普通 Java 字符串是不可变的,而 GString 的结果 String 表示形式可能会有所不同,具体取决于其插值值。即使对于相同的字符串结果,GString 和 String 也没有相同的哈希码。
assert "one: ${1}".hashCode() != "one: 1".hashCode()
由于 GString 和 String 具有不同的哈希码值,因此应避免使用 GString 作为 Map 密钥,尤其是当我们尝试使用 String 而不是 GString 检索关联的值时。
def key = "a"
def m = ["${key}": "letter ${key}"] (1)
assert m["a"] == null (2)
1 | 该地图使用一个初始对创建,该对的键是一个 GString |
2 | 当我们尝试使用 String 键获取值时,将找不到它,因为 String 和 GString 具有不同的哈希码值 |
4.5. 三重双引号字符串
三重双引号字符串的行为类似于双引号字符串,另外它们是多行的,就像三重单引号字符串一样。
def name = 'Groovy'
def template = """
Dear Mr ${name},
You're the winner of the lottery!
Yours sincerly,
Dave
"""
assert template.toString().contains('Groovy')
在三重双引号字符串中,不需要转义双引号或单引号。 |
4.6. 斜线字符串
除了通常的引号字符串之外,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'
4.6.1. 特殊情况
空斜线字符串不能用双斜杠表示,因为它被 Groovy 解析器理解为行注释。这就是为什么以下断言实际上不会编译,因为它看起来像一个未终止的语句
assert '' == //
由于斜线字符串主要用于使正则表达式更容易使用,因此在 GString 中错误的某些内容(如$()
或$5
)将在斜线字符串中起作用。
请记住,不需要转义反斜杠。另一种思考方式是,实际上不支持转义。斜线字符串/\t/
不会包含一个制表符,而是包含一个反斜杠,后面跟着字符 't'。只允许对斜杠字符进行转义,即/\/folder/
将是一个包含'/folder'
的斜线字符串。斜杠转义的结果是,斜线字符串不能以反斜杠结尾。否则,这将转义斜线字符串终止符。你可以改用一个特殊的技巧,/ends with slash ${'\'}/
。但最好在这种情况下去掉斜线字符串。
4.7. 美元斜线字符串
美元斜线字符串是多行 GString,用一个起始的$/
和一个结束的/$
分隔。转义字符是美元符号,它可以转义另一个美元符号,或一个正斜杠。仅在与这些字符的特殊用法发生冲突时,才需要对美元符号和正斜杠字符进行转义。字符$foo
通常表示 GString 占位符,因此可以通过转义美元符号将这四个字符输入美元斜线字符串,即$$foo
。类似地,如果你想在字符串中使用美元斜线字符串结束分隔符,则需要转义它。
以下是一些示例
def name = "Guillaume"
def date = "April, 1st"
def dollarSlashy = $/
Hello $name,
today we're ${date}.
$ dollar sign
$$ escaped dollar sign
\ backslash
/ forward slash
$/ escaped forward slash
$$$/ escaped opening dollar slashy
$/$$ escaped closing dollar slashy
/$
assert [
'Guillaume',
'April, 1st',
'$ dollar sign',
'$ escaped dollar sign',
'\\ backslash',
'/ forward slash',
'/ escaped forward slash',
'$/ escaped opening dollar slashy',
'/$ escaped closing dollar slashy'
].every { dollarSlashy.contains(it) }
它被创建是为了克服斜线字符串转义规则的一些限制。当它的转义规则适合你的字符串内容时使用它(通常是如果它有一些你不想转义的斜杠)。
4.8. 字符串摘要表
字符串名称 |
字符串语法 |
插值 |
多行 |
转义字符 |
单引号 |
|
|
||
三重单引号 |
|
|
||
双引号 |
|
|
||
三重双引号 |
|
|
||
斜线 |
|
|
||
美元斜线 |
|
|
4.9. 字符
与 Java 不同,Groovy 没有明确的字符文字。但是,你可以通过三种不同的方法明确地将 Groovy 字符串设为一个实际的字符
char c1 = 'A' (1)
assert c1 instanceof Character
def c2 = 'B' as char (2)
assert c2 instanceof Character
def c3 = (char)'C' (3)
assert c3 instanceof Character
1 | 在声明保存字符的变量时,通过指定char 类型来明确指定 |
2 | 使用as 运算符进行类型强制转换 |
3 | 使用强制转换为 char 运算 |
第一个选项1在字符保存在变量中时很有趣,而其他两个选项(2和3)在必须将 char 值作为方法调用的参数传递时更有趣。 |
5. 数字
Groovy 支持不同类型的整型文字和十进制文字,它们由 Java 中的常用Number
类型支持。
5.1. 整型文字
整型文字类型与 Java 中的相同
-
byte
-
char
-
short
-
int
-
long
-
java.math.BigInteger
你可以使用以下声明创建这些类型的整型数字
// primitive types
byte b = 1
char c = 2
short s = 3
int i = 4
long l = 5
// infinite precision
BigInteger bi = 6
如果你使用def
关键字进行可选类型化,则整型数字的类型会有所不同:它将适应可以保存该数字的类型的容量。
对于正数
def a = 1
assert a instanceof Integer
// Integer.MAX_VALUE
def b = 2147483647
assert b instanceof Integer
// Integer.MAX_VALUE + 1
def c = 2147483648
assert c instanceof Long
// Long.MAX_VALUE
def d = 9223372036854775807
assert d instanceof Long
// Long.MAX_VALUE + 1
def e = 9223372036854775808
assert e instanceof BigInteger
以及负数
def na = -1
assert na instanceof Integer
// Integer.MIN_VALUE
def nb = -2147483648
assert nb instanceof Integer
// Integer.MIN_VALUE - 1
def nc = -2147483649
assert nc instanceof Long
// Long.MIN_VALUE
def nd = -9223372036854775808
assert nd instanceof Long
// Long.MIN_VALUE - 1
def ne = -9223372036854775809
assert ne instanceof BigInteger
5.1.1. 替代非十进制表示法
数字也可以用二进制、八进制、十六进制和十进制表示。
二进制文字
二进制数字以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
5.2. 十进制字面量
十进制字面量类型与 Java 中相同
-
float
-
double
-
java.math.BigDecimal
您可以使用以下声明创建这些类型的十进制数字
// primitive types
float f = 1.234
double d = 2.345
// infinite precision
BigDecimal bd = 3.456
十进制数字可以使用指数,带有e
或E
指数字母,后跟可选符号,以及表示指数的整数
assert 1e3 == 1_000.0
assert 2E4 == 20_000.0
assert 3e+1 == 30.0
assert 4E-2 == 0.04
assert 5e-1 == 0.5
为方便精确的十进制数字计算,Groovy 选择java.math.BigDecimal
作为其十进制数字类型。此外,还支持float
和double
,但需要显式类型声明、类型强制转换或后缀。即使BigDecimal
是十进制数字的默认值,此类字面量也可以在接受float
或double
作为参数类型的函数或闭包中使用。
十进制数字不能使用二进制、八进制或十六进制表示。 |
5.3. 字面量中的下划线
在编写长字面量数字时,很难一眼看出一些数字是如何分组的,例如成千上万、单词等。允许您在数字字面量中放置下划线,可以更容易地识别这些组
long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010
5.4. 数字类型后缀
我们可以通过添加后缀(见下表),强制数字(包括二进制、八进制和十六进制)具有特定类型,可以使用大写或小写。
类型 | 后缀 |
---|---|
BigInteger |
|
Long |
|
Integer |
|
BigDecimal |
|
Double |
|
Float |
|
示例
assert 42I == Integer.valueOf('42')
assert 42i == Integer.valueOf('42') // lowercase i more readable
assert 123L == Long.valueOf("123") // uppercase L more readable
assert 2147483648 == Long.valueOf('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 .321 == new BigDecimal('.321')
assert 1.200065D == Double.valueOf('1.200065')
assert 1.234F == Float.valueOf('1.234')
assert 1.23E23D == Double.valueOf('1.23E23')
assert 0b1111L.class == Long // binary
assert 0xFFi.class == Integer // hexadecimal
assert 034G.class == BigInteger // octal
5.5. 数学运算
虽然运算符在其他地方有更详细的介绍,但讨论数学运算的行为及其结果类型很重要。
除了除法和幂二元运算(如下所述)外,
-
byte
、char
、short
和int
之间的二元运算结果为int
-
long
与byte
、char
、short
和int
的二元运算结果为long
-
BigInteger
与任何其他整型类型的二元运算结果为BigInteger
-
BigDecimal
与byte
、char
、short
、int
和BigInteger
的二元运算结果为BigDecimal
-
float
、double
和BigDecimal
之间的二元运算结果为double
-
两个
BigDecimal
之间的二元运算结果为BigDecimal
下表总结了这些规则
byte | char | short | int | long | BigInteger | float | double | BigDecimal | |
---|---|---|---|---|---|---|---|---|---|
byte |
int |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
char |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|
short |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
||
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|||
long |
long |
BigInteger |
double |
double |
BigDecimal |
||||
BigInteger |
BigInteger |
double |
double |
BigDecimal |
|||||
float |
double |
double |
double |
||||||
double |
double |
double |
|||||||
BigDecimal |
BigDecimal |
由于 Groovy 的运算符重载,常见的算术运算符也适用于BigInteger 和BigDecimal ,这与 Java 不同,在 Java 中您必须使用显式方法来对这些数字进行运算。 |
5.5.1. 除法运算符的情况
除法运算符/
(和/=
用于除法和赋值)如果任一操作数为float
或double
,则产生double
结果,否则产生BigDecimal
结果(当两个操作数都是整型short
、char
、byte
、int
、long
、BigInteger
或BigDecimal
的任何组合时)。
BigDecimal
除法使用divide()
方法执行,如果除法是精确的(即产生一个可以在相同精度和比例范围内表示的结果),或者使用具有精度为两个操作数精度的最大值加上额外精度 10 以及比例为 10 和操作数比例的最大值的MathContext
。
对于像 Java 中那样的整数除法,您应该使用intdiv() 方法,因为 Groovy 没有提供专用的整数除法运算符符号。 |
5.5.2. 幂运算符的情况
幂运算用**
运算符表示,有两个参数:底数和指数。幂运算的结果取决于其操作数,以及运算的结果(特别是结果是否可以表示为整数值)。
Groovy 的幂运算使用以下规则来确定结果类型
-
如果指数是十进制值
-
如果结果可以表示为
Integer
,则返回Integer
-
否则,如果结果可以表示为
Long
,则返回Long
-
否则返回
Double
-
-
如果指数是整数值
-
如果指数严格为负数,则如果结果值适合该类型,则返回
Integer
、Long
或Double
-
如果指数为正数或零
-
如果底数为
BigDecimal
,则返回BigDecimal
结果值 -
如果底数为
BigInteger
,则返回BigInteger
结果值 -
如果底数为
Integer
,则如果结果值适合它,则返回Integer
,否则返回BigInteger
-
如果底数为
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
7. 列表
Groovy 使用用方括号括起来的逗号分隔的值列表来表示列表。Groovy 列表是普通的 JDK java.util.List
,因为 Groovy 没有定义自己的集合类。在定义列表字面量时使用的具体列表实现默认情况下是java.util.ArrayList
,除非您决定另行指定,我们将在后面看到。
def numbers = [1, 2, 3] (1)
assert numbers instanceof List (2)
assert numbers.size() == 3 (3)
1 | 我们定义一个用逗号分隔并用方括号括起来的数字列表,并将该列表赋值给一个变量 |
2 | 该列表是 Java 的java.util.List 接口的实例 |
3 | 可以使用size() 方法查询列表的大小,并显示我们的列表包含 3 个元素 |
在上面的示例中,我们使用了一个同构列表,但您也可以创建包含异构类型值的列表
def heterogeneous = [1, "a", true] (1)
1 | 我们这里的列表包含一个数字、一个字符串和一个布尔值 |
我们提到,默认情况下,列表字面量实际上是java.util.ArrayList
的实例,但可以使用as
运算符进行类型强制转换,或者使用变量的显式类型声明,为我们的列表使用不同的支持类型
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList
def linkedList = [2, 3, 4] as LinkedList (1)
assert linkedList instanceof java.util.LinkedList
LinkedList otherLinked = [3, 4, 5] (2)
assert otherLinked instanceof java.util.LinkedList
1 | 我们使用as 运算符进行强制转换来显式请求java.util.LinkedList 实现 |
2 | 我们可以说,持有列表字面量的变量的类型为java.util.LinkedList |
可以使用[]
下标运算符(用于读取和设置值,都可使用)访问列表的元素,可以使用正索引或负索引来访问列表末端的元素,也可以使用范围,并使用<<
左移运算符将元素追加到列表中
def letters = ['a', 'b', 'c', 'd']
assert letters[0] == 'a' (1)
assert letters[1] == 'b'
assert letters[-1] == 'd' (2)
assert letters[-2] == 'c'
letters[2] = 'C' (3)
assert letters[2] == 'C'
letters << 'e' (4)
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
assert letters[1, 3] == ['b', 'd'] (5)
assert letters[2..4] == ['C', 'd', 'e'] (6)
1 | 访问列表的第一个元素(从零开始计数) |
2 | 使用负索引访问列表的最后一个元素:-1 是从列表末端开始的第一个元素 |
3 | 使用赋值为列表的第三个元素设置新值 |
4 | 使用<< 左移运算符将元素追加到列表的末尾 |
5 | 一次访问两个元素,返回包含这两个元素的新列表 |
6 | 使用范围访问列表中的值范围,从开始元素位置到结束元素位置 |
由于列表本质上可以是异构的,因此列表也可以包含其他列表来创建多维列表
def multi = [[0, 1], [2, 3]] (1)
assert multi[1][0] == 2 (2)
1 | 定义一个数字列表 |
2 | 访问最顶层列表的第二个元素,以及内部列表的第一个元素 |
8. 数组
Groovy 重新使用列表表示法来表示数组,但为了使这些字面量成为数组,您需要通过强制转换或类型声明来显式定义数组的类型。
String[] arrStr = ['Ananas', 'Banana', 'Kiwi'] (1)
assert arrStr instanceof String[] (2)
assert !(arrStr instanceof List)
def numArr = [1, 2, 3] as int[] (3)
assert numArr instanceof int[] (4)
assert numArr.size() == 3
1 | 使用显式变量类型声明定义一个字符串数组 |
2 | 断言我们创建了一个字符串数组 |
3 | 使用as 运算符创建整数数组 |
4 | 断言我们创建了一个原始整数数组 |
您也可以创建多维数组
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 | 将数组的第三个元素的值设置为新值 |
8.1. Java 风格的数组初始化
Groovy 一直支持使用方括号的字面量列表/数组定义,并避免了 Java 风格的花括号,以避免与闭包定义冲突。但是,在花括号紧跟在数组类型声明之后的情况下,与闭包定义不会出现歧义,因此 Groovy 3 及更高版本支持这种 Java 数组初始化表达式的变体。
示例
def primes = new int[] {2, 3, 5, 7, 11}
assert primes.size() == 5 && primes.sum() == 28
assert primes.class.name == '[I'
def pets = new String[] {'cat', 'dog'}
assert pets.size() == 2 && pets.sum() == 'catdog'
assert pets.class.name == '[Ljava.lang.String;'
// traditional Groovy alternative still supported
String[] groovyBooks = [ 'Groovy in Action', 'Making Java Groovy' ]
assert groovyBooks.every{ it.contains('Groovy') }
9. 映射
在其他语言中,有时称为字典或关联数组,Groovy 具有映射。映射将键与值相关联,用冒号分隔键和值,每个键/值对用逗号分隔,整个键和值用方括号括起来。
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'] (1)
assert colors['red'] == '#FF0000' (2)
assert colors.green == '#00FF00' (3)
colors['pink'] = '#FF00FF' (4)
colors.yellow = '#FFFF00' (5)
assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'
assert colors instanceof java.util.LinkedHashMap
1 | 我们定义一个字符串颜色名称的映射,与它们的十六进制编码的 HTML 颜色相关联 |
2 | 我们使用下标表示法来检查与red 键关联的内容 |
3 | 我们也可以使用属性表示法来断言绿色颜色的十六进制表示 |
4 | 类似地,可以使用下标表示法添加新的键/值对 |
5 | 或者属性表示法,添加yellow 颜色 |
在使用键的名称时,我们实际上在映射中定义了字符串键。 |
Groovy 创建的映射实际上是java.util.LinkedHashMap 的实例。 |
如果您尝试访问映射中不存在的键
assert colors.unknown == null
def emptyMap = [:]
assert emptyMap.anyKey == null
您将检索到null
结果。
在上面的示例中,我们使用字符串键,但您也可以使用其他类型的数值作为键
def numbers = [1: 'one', 2: 'two']
assert numbers[1] == 'one'
这里,我们使用数字作为键,因为数字可以明确地识别为数字,所以 Groovy 不会像我们之前的示例那样创建字符串键。但请考虑您想传递一个变量代替键的情况,使该变量的值成为键
def key = 'name'
def person = [key: 'Guillaume'] (1)
assert !person.containsKey('name') (2)
assert person.containsKey('key') (3)
1 | 与 'Guillaume' 名称关联的 key 实际上将是 "key" 字符串,而不是与 key 变量关联的值。 |
2 | 该映射不包含 'name' 键。 |
3 | 相反,该映射包含一个 'key' 键。 |
您也可以传递带引号的字符串以及键:["name": "Guillaume"]。如果您想要创建包含连字符的字符串键(例如,在 ["street-name": "Main street"] 中),则此操作是强制性的。 |
当您需要在映射定义中传递变量值作为键时,必须用括号将变量或表达式括起来。
person = [(key): 'Guillaume'] (1)
assert person.containsKey('name') (2)
assert !person.containsKey('key') (3)
1 | 这次,我们将 key 变量用括号括起来,以指示解析器我们正在传递一个变量,而不是定义一个字符串键。 |
2 | 该映射包含 name 键。 |
3 | 但该映射不再包含 key 键。 |