本章涵盖 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 有以下保留关键字:

表 1. 保留关键字

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

其中,constgotostrictfpthreadsafe 目前未使用。

保留关键字通常不能用于变量、字段和方法名称。

通过将名称用引号括起来,可以定义与关键字同名的方法,如下例所示:

// reserved keywords can be used for method names if quoted
def "abstract"() { true }
// when calling such methods, the name must be qualified using "this."
this.abstract()

使用此类名称可能会令人困惑,通常最好避免。该技巧主要旨在支持某些 Java 集成场景和某些 DSL 场景,在这些场景中,使用与关键字同名的“动词”和“名词”可能是可取的。

此外,Groovy 还有以下上下文关键字:

表 2. 上下文关键字

as

in

permits

record

sealed

trait

var

yields

这些词只在特定上下文中是关键字,在某些地方可以更自由地使用,特别是对于变量、字段和方法名称。

这种额外的宽松允许使用在 Groovy 早期版本中不是关键字或在 Java 中不是关键字的方法或变量名称。示例如下:

// contextual keywords can be used for field and variable names
def as = true
assert as

// contextual keywords can be used for method names
def in() { true }
// when calling such methods, the name only needs to be qualified using "this." in scenarios which would be ambiguous
this.in()

熟悉这些上下文关键字的 Groovy 程序员可能仍然希望避免使用这些名称,除非有充分的理由使用此类名称。

保留关键字的限制也适用于原始类型、布尔字面量和 null 字面量(所有这些都将在后面讨论)

表 3. 其他保留字

null

true

false

boolean

char

byte

short

int

long

float

double

虽然不推荐,但可以使用与保留关键字相同的技巧:

def "null"() { true }  // not recommended; potentially confusing
assert this.null()     // must be qualified

将这些词用作方法名称可能会令人困惑,通常最好避免,但是,它可能对某些类型的 DSL 有用。

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.1. 单引号字符串

单引号字符串是由单引号括起来的一系列字符:

'a single-quoted string'
单引号字符串是普通的 java.lang.String,不支持插值。

4.2. 字符串连接

所有 Groovy 字符串都可以使用 + 运算符进行连接:

assert 'ab' == 'a' + 'b'

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 我们定义一个包含 1number 变量,然后将其插值到两个 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 在字符保存在变量中时很有趣,而另外两个选项(23)在 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

小数可以使用指数,以 eE 指数字母开头,后跟一个可选的符号,以及一个表示指数的整数:

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 作为其小数类型。此外,floatdouble 也受支持,但需要显式类型声明、类型强制转换或后缀。即使 BigDecimal 是小数的默认值,此类字面量也可以在接受 floatdouble 作为参数类型的方法或闭包中接受。

小数不能使用二进制、八进制或十六进制表示。

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

Gg

Long

Ll

Integer

Ii

BigDecimal

Gg

Double

Dd

Float

Ff

示例

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. 数学运算

尽管运算符在其他地方有更详细的介绍,但讨论数学运算的行为及其结果类型很重要。

除了除法和幂的二元运算(下文将讨论)之外,

  • bytecharshortint 之间的二元运算结果为 int

  • 涉及 longbytecharshortint 的二元运算结果为 long

  • 涉及 BigInteger 和任何其他整数类型的二元运算结果为 BigInteger

  • 涉及 BigDecimalbytecharshortintBigInteger 的二元运算结果为 BigDecimal

  • floatdoubleBigDecimal 之间的二元运算结果为 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 的运算符重载,常用的算术运算符也适用于 BigIntegerBigDecimal,这与 Java 不同,在 Java 中您必须使用显式方法来操作这些数字。

5.5.1. 除法运算符的情况

除法运算符 /(以及用于除法和赋值的 /=)如果任一操作数是 floatdouble,则产生 double 结果;否则(当两个操作数都是整数类型 shortcharbyteintlongBigIntegerBigDecimal 的任意组合时),则产生 BigDecimal 结果。

BigDecimal 除法如果精确(即产生的结果可以在相同精度和范围的范围内表示),则使用 divide() 方法执行;否则,使用具有两个操作数的最大精度加上额外的 10 精度,以及 10 和操作数最大范围的最大值作为范围MathContext 执行。

对于像 Java 那样的整数除法,您应该使用 intdiv() 方法,因为 Groovy 没有提供专门的整数除法运算符符号。

5.5.2. 幂运算符的情况

幂运算由 ** 运算符表示,它有两个参数:底数和指数。幂运算的结果取决于其操作数,以及运算结果(特别是如果结果可以表示为整数值)。

Groovy 的幂运算使用以下规则来确定结果类型:

  • 如果指数是小数:

    • 如果结果可以表示为 Integer,则返回 Integer

    • 否则,如果结果可以表示为 Long,则返回 Long

    • 否则返回 Double

  • 如果指数是整数值:

    • 如果指数严格为负数,则返回 IntegerLongDouble,如果结果值符合该类型

    • 如果指数为正或零:

      • 如果底数是 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

6. 布尔值

布尔值是一种特殊的数据类型,用于表示真值:truefalse。将此数据类型用于跟踪真/假条件的简单标志。

布尔值可以存储在变量中,赋值给字段,就像任何其他数据类型一样:

def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true

truefalse 是仅有的两个原始布尔值。但是可以使用逻辑运算符表示更复杂的布尔表达式。

此外,Groovy 有特殊规则(通常称为 Groovy Truth)用于将非布尔对象强制转换为布尔值。

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 键: