本章涵盖 Groovy 编程语言的语法。该语言的语法源自 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 相同的标签。
此外,Groovy 自 3.0.0 版本起支持 运行时 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 字面量(所有这些都将在后面讨论)
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
结果;否则(当两个操作数都是整数类型 short
、char
、byte
、int
、long
、BigInteger
或 BigDecimal
的任意组合时),则产生 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 运算符创建 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 | 将数组的第三个元素的值设置为新值: |
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 键: |