语法

本章涵盖 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 的 GStrings(插值字符串)之间存在差异,因为在后一种情况下,插值值被插入到最终字符串中以评估整个标识符

def firstname = "Homer"
map."Simpson-${firstname}" = "Homer Simpson"

assert map.'Simpson-Homer' == "Homer Simpson"

4. 字符串

文本字面量以字符链的形式表示,称为字符串。Groovy 允许您实例化 java.lang.String 对象,以及 GStrings (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.ba.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 的结果字符串表示可以根据其插值值而变化。即使对于相同的最终字符串,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 '' == //

由于斜杠字符串主要旨在简化正则表达式,因此 GStrings 中的一些错误(如 $()$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 结果。

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

对于像 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。将此数据类型用于跟踪 true/false 条件 的简单标志。

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

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