语法

本章介绍 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 获取类的运行时 Groovydoc Foo
2 获取方法的运行时 Groovydoc bar

1.4 Shebang 行

除了单行注释之外,还有一种特殊行注释,通常称为 UNIX 系统理解的 shebang 行,它允许直接从命令行运行脚本,前提是您已经安装了 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 传递给一个接受字符串的方法,则占位符内的表达式值将被求值为其字符串表示形式(通过对该表达式调用 toString()),并将结果字符串传递给该方法。

这里有一个字符串,其中占位符引用局部变量

def name = 'Guillaume' // a plain string
def greeting = "Hello ${name}"

assert greeting.toString() == 'Hello Guillaume'

任何 Groovy 表达式都是有效的,正如我们在具有算术表达式的示例中看到的那样

def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'
不仅允许在 ${} 占位符之间有表达式,而且还允许有语句。但是,语句的值只是 null。因此,如果在该占位符中插入多个语句,则最后一个语句应该以某种方式返回一个有意义的值才能插入。例如,“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. Closure 表达式内插的特殊情况

到目前为止,我们已经看到可以在${}占位符中内插任意表达式,但是对于闭包表达式有特殊的情况和表示法。当占位符包含一个箭头 ${→} 时,该表达式实际上是一个闭包表达式 - 您可以将其视为一个在前面添加了美元符号的闭包

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自变量,您可以使用<<leftShift 运算符为其追加内容。在任一种情况下,这两个占位符都是嵌入式闭包。

从外观上看,它看起来像是定义要插值的表达式的更详细的方法,但闭包比单纯的表达式有有趣的优势:惰性求值。

我们考虑以下示例

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 我们定义一个变量number,其中包含1,然后将其作为eagerGString中的表达式和lazyGString中的闭包内插到两个 GString 中。
2 我们期望对于eagerGString,结果字符串包含相同的字符串值 1。
3 对于lazyGString也类似
4 然后,我们将变量的值更改为新编号
5 使用普通内插表达式时,该值实际上在创建 GString 时绑定。
6 但使用闭包表达式时,每次将 GString 转换为字符串时,都会调用该闭包,从而生成包含新数字值的新字符串。
采用多个参数的嵌入式闭包表达式将在运行时生成异常。仅允许采用 0 个或 1 个参数的闭包。

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 hashCodes

虽然插值字符串可用于替换普通 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 '' == //

由于带斜杠字符串主要设计为简化 regexp,因此 GString 语言中的部分错误(例如 $()$5)在带斜杠字符串中即可使用。

请记住,无需转义反斜杠。另一种理解方式是实际上不支持转义。带斜杠字符串 /\t/ 将不包含制表符,而是反斜杠后跟字符 't'。仅允许转义斜杠字符,即 /\/folder/ 将为带斜杠字符串,包含 '/folder'。斜杠转义的后果是带斜杠字符串不能以反斜杠结尾。否则将会转义带斜杠字符串终止符。您还可以使用特殊技巧 /ends with slash ${'\'}/。但在这种情况下,最好避免使用带斜杠字符串。

4.7. Dollar slashy 字符串

Dollar slashy 字符串是多行 GString,以一个开头 $/ 和一个结尾 /$。转义字符是美元符号,它可以转义另一个美元符号或一个正斜杠。美元符号和正斜杠字符的转义仅在字符的特殊使用出现冲突时才需要。通常,$foo 字符指示一个 GString 占位符,因此,可以通过转义美元符号 $$foo 进入一个美元 slashy 字符串中。类似地,如果你希望它出现在你的字符串中,你需要转义一个美元 slashy 结束分隔符。

下面是一些示例

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) }

它旨在克服 slashy 字符串转义规则的一些限制。当它的转义规则适合你的字符串内容(通常情况下,如果它有一些你不想转义的斜杠) 时,请使用它。

4.8. 字符串汇总表

字符串名称

字符串语法

插值

多行

转义字符

单引号

'…​'

\

三重单引号

'''…​'''

\

双引号

"…​"

\

三重双引号

"""…​"""

\

Slashy

/…​/

\

Dollar slashy

$/…​/$

$

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 通过使用一个 cast to char 操作
当字符存储在一个变量中时,第一个选项 1 是有意义的,而当 char 值必须作为方法调用的参数传递时,另外两个选项(23)更有意义。

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. 备用非 10 进制表示

数字还可以用二进制、八进制、十六进制和小数为基础表示。

二进制字面量

二进制数以 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

  • 包含BigDecimalbytecharshortint以及BigInteger的二进制运算将返回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 不同,后者对这些数字的操作必须使用显式方法。

5.5.1 除法运算符的情况

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

如果除法精确(即生成的结果可在同精度和规模的界限内表示),则BigDecimal除法将使用divide()方法执行,否则将使用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. 布尔值

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

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

def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true

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

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

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

您可以使用 [] 下标运算符(用于读写值)访问列表的元素,也可以使用正向索引或负向索引来访问列表末尾的元素,还可以使用范围,并使用 << leftShift 运算符将元素附加到列表中

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 使用 << leftShift 运算符在列表末尾附加一个元素
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 或者属性符号,添加黄色颜色
在使用名称表示键时,我们实际在图中定义字符串键。
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