本章介绍 Groovy 编程语言的语法。该语言的语法源于 Java 语法,但在 Java 语法基础上添加了 Groovy 特定的结构,并允许进行某些简化。

1. 注释

1.1. 单行注释

单行注释以 // 开头,可以出现在行中的任何位置。// 之后的字符(直到行尾)都被视为注释的一部分。

// a standalone single line comment
println "hello" // a comment till the end of the line

1.2. 多行注释

多行注释以 /* 开头,可以出现在行中的任何位置。/* 之后的字符(包括换行符)直到第一个 */(关闭注释)都被视为注释的一部分。因此,多行注释可以放在语句末尾,甚至放在语句内部。

/* a standalone multiline comment
   spanning two lines */
println "hello" /* a multiline comment starting
                   at the end of a statement */
println 1 /* one */ + 2 /* two */

1.3. Groovydoc 注释

与多行注释类似,Groovydoc 注释也是多行的,但以 /** 开头,以 */ 结尾。第一个 Groovydoc 注释行后面的行可以(可选)以星号 * 开头。这些注释与以下语言元素相关联:

  • 类型定义(类、接口、枚举、注解),

  • 字段和属性定义

  • 方法定义

虽然编译器不会对与上述语言元素无关的 Groovydoc 注释发出警告,但你应该在这些结构之前添加注释,紧挨着它。

/**
 * A Class description
 */
class Person {
    /** the name of the person */
    String name

    /**
     * Creates a greeting method for a certain person.
     *
     * @param otherPerson the person to greet
     * @return a greeting message
     */
    String greet(String otherPerson) {
       "Hello ${otherPerson}"
    }
}

Groovydoc 遵循与 Java 的 Javadoc 相同的约定。因此,你可以使用与 Javadoc 相同的标签。

此外,从 3.0.0 版本开始,Groovy 支持**运行时 Groovydoc**,即 Groovydoc 可以保留在运行时。

默认情况下,运行时 Groovydoc 处于禁用状态。可以通过添加 JVM 选项 -Dgroovy.attach.runtime.groovydoc=true 来启用它。

运行时 Groovydoc 以 /**@ 开头,以 */ 结尾,例如:

/**@
 * Some class groovydoc for Foo
 */
class Foo {
    /**@
     * Some method groovydoc for bar
     */
    void bar() {
    }
}

assert Foo.class.groovydoc.content.contains('Some class groovydoc for Foo') (1)
assert Foo.class.getMethod('bar', new Class[0]).groovydoc.content.contains('Some method groovydoc for bar') (2)
1 获取类 Foo 的运行时 groovydoc
2 获取方法 bar 的运行时 groovydoc

1.4. Shebang 行

除了单行注释外,还有一种特殊的行注释,通常被称为shebang 行,它被 UNIX 系统理解,允许脚本直接从命令行运行,前提是你已安装 Groovy 发行版,并且 groovy 命令在 PATH 上可用。

#!/usr/bin/env groovy
println "Hello from the shebang line"
# 字符必须是文件的第一个字符。任何缩进都会导致编译错误。

2. 关键字

Groovy 具有以下保留关键字

表 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 程序员可能仍希望避免使用这些名称,除非有充分的理由使用它们。

对保留关键字的限制也适用于基本类型、布尔值字面量和空值字面量(所有这些将在后面讨论)。

表 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.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 的结果 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结果,否则产生BigDecimal结果(当两个操作数都是整型shortcharbyteintlongBigIntegerBigDecimal的任何组合时)。

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 真值)用于将非布尔对象强制转换为布尔值。

7. 列表

Groovy 使用用方括号括起来的逗号分隔的值列表来表示列表。Groovy 列表是普通的 JDK java.util.List,因为 Groovy 没有定义自己的集合类。在定义列表字面量时使用的具体列表实现默认情况下是java.util.ArrayList,除非您决定另行指定,我们将在后面看到。

def numbers = [1, 2, 3]         (1)

assert numbers instanceof List  (2)
assert numbers.size() == 3      (3)
1 我们定义一个用逗号分隔并用方括号括起来的数字列表,并将该列表赋值给一个变量
2 该列表是 Java 的java.util.List接口的实例
3 可以使用size()方法查询列表的大小,并显示我们的列表包含 3 个元素

在上面的示例中,我们使用了一个同构列表,但您也可以创建包含异构类型值的列表

def heterogeneous = [1, "a", true]  (1)
1 我们这里的列表包含一个数字、一个字符串和一个布尔值

我们提到,默认情况下,列表字面量实际上是java.util.ArrayList的实例,但可以使用as运算符进行类型强制转换,或者使用变量的显式类型声明,为我们的列表使用不同的支持类型

def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList

def linkedList = [2, 3, 4] as LinkedList    (1)
assert linkedList instanceof java.util.LinkedList

LinkedList otherLinked = [3, 4, 5]          (2)
assert otherLinked instanceof java.util.LinkedList
1 我们使用as运算符进行强制转换来显式请求java.util.LinkedList实现
2 我们可以说,持有列表字面量的变量的类型为java.util.LinkedList

可以使用[]下标运算符(用于读取和设置值,都可使用)访问列表的元素,可以使用正索引或负索引来访问列表末端的元素,也可以使用范围,并使用<<左移运算符将元素追加到列表中

def letters = ['a', 'b', 'c', 'd']

assert letters[0] == 'a'     (1)
assert letters[1] == 'b'

assert letters[-1] == 'd'    (2)
assert letters[-2] == 'c'

letters[2] = 'C'             (3)
assert letters[2] == 'C'

letters << 'e'               (4)
assert letters[ 4] == 'e'
assert letters[-1] == 'e'

assert letters[1, 3] == ['b', 'd']         (5)
assert letters[2..4] == ['C', 'd', 'e']    (6)
1 访问列表的第一个元素(从零开始计数)
2 使用负索引访问列表的最后一个元素:-1 是从列表末端开始的第一个元素
3 使用赋值为列表的第三个元素设置新值
4 使用<<左移运算符将元素追加到列表的末尾
5 一次访问两个元素,返回包含这两个元素的新列表
6 使用范围访问列表中的值范围,从开始元素位置到结束元素位置

由于列表本质上可以是异构的,因此列表也可以包含其他列表来创建多维列表

def multi = [[0, 1], [2, 3]]     (1)
assert multi[1][0] == 2          (2)
1 定义一个数字列表
2 访问最顶层列表的第二个元素,以及内部列表的第一个元素

8. 数组

Groovy 重新使用列表表示法来表示数组,但为了使这些字面量成为数组,您需要通过强制转换或类型声明来显式定义数组的类型。

String[] arrStr = ['Ananas', 'Banana', 'Kiwi']  (1)

assert arrStr instanceof String[]    (2)
assert !(arrStr instanceof List)

def numArr = [1, 2, 3] as int[]      (3)

assert numArr instanceof int[]       (4)
assert numArr.size() == 3
1 使用显式变量类型声明定义一个字符串数组
2 断言我们创建了一个字符串数组
3 使用as运算符创建整数数组
4 断言我们创建了一个原始整数数组

您也可以创建多维数组

def matrix3 = new Integer[3][3]         (1)
assert matrix3.size() == 3

Integer[][] matrix2                     (2)
matrix2 = [[1, 2], [3, 4]]
assert matrix2 instanceof Integer[][]
1 您可以定义新数组的边界
2 或者声明一个不指定其边界的数组

访问数组元素的表示法与列表相同

String[] names = ['Cédric', 'Guillaume', 'Jochen', 'Paul']
assert names[0] == 'Cédric'     (1)

names[2] = 'Blackdrag'          (2)
assert names[2] == 'Blackdrag'
1 检索数组的第一个元素
2 将数组的第三个元素的值设置为新值

8.1. Java 风格的数组初始化

Groovy 一直支持使用方括号的字面量列表/数组定义,并避免了 Java 风格的花括号,以避免与闭包定义冲突。但是,在花括号紧跟在数组类型声明之后的情况下,与闭包定义不会出现歧义,因此 Groovy 3 及更高版本支持这种 Java 数组初始化表达式的变体。

示例

def primes = new int[] {2, 3, 5, 7, 11}
assert primes.size() == 5 && primes.sum() == 28
assert primes.class.name == '[I'

def pets = new String[] {'cat', 'dog'}
assert pets.size() == 2 && pets.sum() == 'catdog'
assert pets.class.name == '[Ljava.lang.String;'

// traditional Groovy alternative still supported
String[] groovyBooks = [ 'Groovy in Action', 'Making Java Groovy' ]
assert groovyBooks.every{ it.contains('Groovy') }

9. 映射

在其他语言中,有时称为字典或关联数组,Groovy 具有映射。映射将键与值相关联,用冒号分隔键和值,每个键/值对用逗号分隔,整个键和值用方括号括起来。

def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']   (1)

assert colors['red'] == '#FF0000'    (2)
assert colors.green  == '#00FF00'    (3)

colors['pink'] = '#FF00FF'           (4)
colors.yellow  = '#FFFF00'           (5)

assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'

assert colors instanceof java.util.LinkedHashMap
1 我们定义一个字符串颜色名称的映射,与它们的十六进制编码的 HTML 颜色相关联
2 我们使用下标表示法来检查与red键关联的内容
3 我们也可以使用属性表示法来断言绿色颜色的十六进制表示
4 类似地,可以使用下标表示法添加新的键/值对
5 或者属性表示法,添加yellow颜色
在使用键的名称时,我们实际上在映射中定义了字符串键。
Groovy 创建的映射实际上是java.util.LinkedHashMap的实例。

如果您尝试访问映射中不存在的键

assert colors.unknown == null

def emptyMap = [:]
assert emptyMap.anyKey == null

您将检索到null结果。

在上面的示例中,我们使用字符串键,但您也可以使用其他类型的数值作为键

def numbers = [1: 'one', 2: 'two']

assert numbers[1] == 'one'

这里,我们使用数字作为键,因为数字可以明确地识别为数字,所以 Groovy 不会像我们之前的示例那样创建字符串键。但请考虑您想传递一个变量代替键的情况,使该变量的值成为键

def key = 'name'
def person = [key: 'Guillaume']      (1)

assert !person.containsKey('name')   (2)
assert person.containsKey('key')     (3)
1 'Guillaume' 名称关联的 key 实际上将是 "key" 字符串,而不是与 key 变量关联的值。
2 该映射不包含 'name' 键。
3 相反,该映射包含一个 'key' 键。
您也可以传递带引号的字符串以及键:["name": "Guillaume"]。如果您想要创建包含连字符的字符串键(例如,在 ["street-name": "Main street"] 中),则此操作是强制性的。

当您需要在映射定义中传递变量值作为键时,必须用括号将变量或表达式括起来。

person = [(key): 'Guillaume']        (1)

assert person.containsKey('name')    (2)
assert !person.containsKey('key')    (3)
1 这次,我们将 key 变量用括号括起来,以指示解析器我们正在传递一个变量,而不是定义一个字符串键。
2 该映射包含 name 键。
3 但该映射不再包含 key 键。