語法

本章涵蓋 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

受保護的

私有的

傳回

靜態的

嚴格浮點

父類

切換

同步的

執行緒安全

擲回

擲回

暫態的

嘗試

其中,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

將此類字詞用作方法名稱可能會造成混淆,因此通常最好避免使用,但對於某些類型的 DSLs 來說,這可能很有用。

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 Development Kit 包含方法,可使用 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 表達式都可以在所有字串文字中內插。內插是將字串中的佔位符替換為字串評估後的其值。佔位符表達式以 ${} 包圍。對於明確的點表示式,可以省略大括號,也就是說,在這些情況下我們可以使用 $ 前綴。

這裡,我們有一個字串,其佔位符參照一個區域變數

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 參數,你可以使用 << 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 我們定義一個包含 1number 變數,然後我們在兩個 GString 中內插它,作為 eagerGString 中的表達式和 lazyGString 中的封閉函式。
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 傳遞給一個將字串作為參數的方法
4 takeString() 方法的簽章明確指出它的唯一參數是一個字串
5 我們也驗證參數確實是一個字串,而不是一個 GString。

4.4.4. GString 和 String 的 hashCodes

雖然內插字串可以用來取代純粹的 Java 字串,但它們在一個特定的方式上與字串不同:它們的 hashCodes 不同。純粹的 Java 字串是不可變的,而 GString 的產生的字串表示法可能會依據它的內插值而有所不同。即使是相同的產生的字串,GString 和 String 也不會有相同的 hashCode。

assert "one: ${1}".hashCode() != "one: 1".hashCode()

由於 GString 和 String 有不同的 hashCode 值,應該避免使用 GString 作為 Map 的鍵,特別是如果我們嘗試使用一個字串而不是一個 GString 來擷取一個關聯值時。

def key = "a"
def m = ["${key}": "letter ${key}"]     (1)

assert m["a"] == null                   (2)
1 這個 map 是使用一個其鍵為 GString 的初始配對建立的
2 當我們嘗試使用一個字串鍵來擷取值時,我們找不到它,因為字串和 GString 有不同的 hashCode 值

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/ 不會包含一個 tab,而是反斜線後接字元 '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. 其他非 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

  • 涉及 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 除法,或使用 precision 為兩個運算元精確度最大值加上額外精確度 10,以及 scale 為 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 類型

您可以使用 [] 下標運算子存取清單的元素(用於讀取和設定數值),使用正向索引或負向索引來存取清單末端的元素,以及使用範圍,並使用 << 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 存取清單的第一個元素(從 0 開始計算)
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 算子建立整數陣列
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