Groovy 4.0 的發行說明

Groovy 4 建立在 Groovy 早期版本的現有功能之上。此外,它還納入了許多新功能,並簡化了 Groovy 程式碼庫的各種舊版方面。

注意
警告:Groovy 4 的一些功能被指定為「孵化中」。在適當的情況下,這些功能的相關類別或 API 可能會加上 @Incubating 註解。
使用孵化中功能時應小心,因為細節可能會在後續版本的 Groovy 中變更。我們不建議將孵化中功能用於生產系統。

重要的命名/結構變更

Maven 座標變更

在 Groovy 4.0 中,Groovy 的 maven 座標的 groupId 已從 org.codehaus.groovy 變更為 org.apache.groovy。請適當地更新您的 Gradle/Maven/其他建置設定。

舊版套件移除

Java 平台模組系統 (JPMS) 要求不同模組中的類別具有不同的套件名稱(稱為「分割套件需求」)。Groovy 有自己的「模組」,而這些模組在歷史上並未根據此需求進行結構化。

Groovy 3 提供了許多類別的重複版本(在舊套件和新套件中),以允許 Groovy 使用者朝向新的 JPMS 相容套件名稱進行移轉。有關更多詳細資訊,請參閱 Groovy 3 發行說明。Groovy 4 不再提供重複的舊版類別。

簡而言之,停止使用 groovy.util.XmlSlurper 並開始使用 groovy.xml.XmlSlurper 的時候到了。類似地,您現在應該使用 groovy.xml.XmlParsergroovy.ant.AntBuildergroovy.test.GroovyTestCase 以及先前提到的 Groovy 3 發行說明中提到的其他類別。

groovy-all 的模組變更

根據使用者的回饋和下載統計資料,我們重新調整了包含在 groovy-all pom 中的模組 (GROOVY-9647)。groovy-yaml 模組被廣泛使用,現已包含在 groovy-all 中。groovy-testng 模組使用較少,不再包含在 groovy-all 中。請視需要調整您的建置指令碼相依性。如果您使用 Groovy 發行版,則不需要變更,因為它包含選用模組。

新功能

切換表達式

Groovy 一直都有非常強大的切換 陳述式,但有時切換 表達式 會更方便。

在切換陳述式中,具有穿透行為的 case 分支通常比處理一個 case 然後跳出切換的分支少見。break 陳述式會讓程式碼雜亂,如下所示。

def result
switch(i) {
  case 0: result = 'zero'; break
  case 1: result = 'one'; break
  case 2: result = 'two'; break
  default: throw new IllegalStateException('unknown number')
}

一個常見的技巧是引入一個方法來包裝切換。在簡單的情況下,多個陳述式可能會簡化為單一回傳陳述式。break 陳述式消失了,儘管被 return 陳述式取代。

def stringify(int i) {
  switch(i) {
    case 0: return 'zero'
    case 1: return 'one'
    case 2: return 'two'
    default: throw new IllegalStateException('unknown number')
  }
}

def result = stringify(i)

切換表達式(大量借用 Java)提供了更好的替代方案

def result = switch(i) {
    case 0 -> 'zero'
    case 1 -> 'one'
    case 2 -> 'two'
    default -> throw new IllegalStateException('unknown number')
}

在此,右手邊(在 -> 之後)必須是單一表達式。如果需要多個陳述式,可以使用區塊。例如,前一個範例的第一個 case 分支可以改寫為

    case 0 -> { def a = 'ze'; def b = 'ro'; a + b }

切換表達式也可以使用傳統的 : 形式,並包含多個陳述式,但這種情況下,必須執行 yield 陳述式。

def result = switch(i) {
    case 0:
        def a = 'ze'
        def b = 'ro'
        if (true) yield a + b
        else yield b + a
    case 1:
        yield 'one'
    case 2:
        yield 'two'
    default:
        throw new IllegalStateException('unknown number')
}

->: 形式不能混合使用。

所有正常的 Groovy case 表達式仍然受到支援,例如

class Custom {
  def isCase(o) { o == -1 }
}

class Coord {
  int x, y
}

def items = [10, -1, 5, null, 41, 3.5f, 38, 99, new Coord(x: 4, y: 5), 'foo']
def result = items.collect { a ->
  switch(a) {
    case null -> 'null'
    case 5 -> 'five'
    case new Custom() -> 'custom'
    case 0..15 -> 'range'
    case [37, 41, 43] -> 'prime'
    case Float -> 'float'
    case { it instanceof Number && it % 2 == 0 } -> 'even'
    case Coord -> a.with { "x: $x, y: $y" }
    case ~/../ -> 'two chars'
    default -> 'none of the above'
  }
}

assert result == ['range', 'custom', 'five', 'null', 'prime', 'float',
                  'even', 'two chars', 'x: 4, y: 5', 'none of the above']

切換表達式特別適用於傳統上可能使用訪問者模式的情況,例如

import groovy.transform.Immutable

interface Expr { }
@Immutable class IntExpr implements Expr { int i }
@Immutable class NegExpr implements Expr { Expr n }
@Immutable class AddExpr implements Expr { Expr left, right }
@Immutable class MulExpr implements Expr { Expr left, right }

int eval(Expr e) {
    e.with {
        switch(it) {
            case IntExpr -> i
            case NegExpr -> -eval(n)
            case AddExpr -> eval(left) + eval(right)
            case MulExpr -> eval(left) * eval(right)
            default -> throw new IllegalStateException()
        }
    }
}

@Newify(pattern=".*Expr")
def test() {
    def exprs = [
        IntExpr(4),
        NegExpr(IntExpr(4)),
        AddExpr(IntExpr(4), MulExpr(IntExpr(3), IntExpr(2))), // 4 + (3*2)
        MulExpr(IntExpr(4), AddExpr(IntExpr(3), IntExpr(2)))  // 4 * (3+2)
    ]
    assert exprs.collect { eval(it) } == [4, -4, 10, 20]
}

test()

與 Java 的差異

  • 目前,沒有要求切換目標的所有可能值都由 case 分支詳盡涵蓋。如果沒有 default 分支,則會新增一個回傳 null 的隱含分支。因此,在不希望有 null 的情況下,例如將結果儲存在基本型別中,或建構不可為 null 的 Optional,則應提供明確的 default,例如

    // default branch avoids GroovyCastException
    int i = switch(s) {
        case 'one' -> 1
        case 'two' -> 2
        default -> 0
    }
    
    // default branch avoids NullPointerException
    Optional.of(switch(i) {
        case 1 -> 'one'
        case 2 -> 'two'
        default -> 'buckle my shoe'
    })

    在未來的 Groovy 版本中,或可能是透過 CodeNarc 等工具,我們預計將支援類似 Java 的更嚴格詳盡 case 分支檢查。這可能會在使用 Groovy 的靜態特性時自動實作,或透過額外的選用型別檢查擴充功能實作。因此,開發人員可能希望不要依賴自動預設分支回傳 null,而應提供自己的預設值或詳盡涵蓋所有分支。

密封類型

密封類別、介面和特質限制其他類別或介面可以延伸或實作它們。Groovy 支援在撰寫密封類型時使用 sealed 關鍵字或 @Sealed 註解。密封類型的允許子類別可以明確給出(使用 sealed 關鍵字的 permits 子句或 @SealedpermittedSubclasses 註解屬性),或者在同時編譯所有相關類型時自動偵測。有關更多詳細資訊,請參閱 (GEP-13) 和 Groovy 文件。

作為一個激勵的範例,密封階層在指定代數或抽象資料類型 (ADT) 時很有用,如下列範例所示(使用註解語法)

import groovy.transform.*

@Sealed interface Tree<T> {}
@Singleton final class Empty implements Tree {
    String toString() { 'Empty' }
}
@Canonical final class Node<T> implements Tree<T> {
    T value
    Tree<T> left, right
}

Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'

作為另一個範例,密封類型在建立增強的 enum 類型階層時很有用。以下是使用 sealed 關鍵字的天氣範例

sealed abstract class Weather { }
class Rainy extends Weather { Integer rainfall }
class Sunny extends Weather { Integer temp }
class Cloudy extends Weather { Integer uvIndex }
def threeDayForecast = [
    new Rainy(rainfall: 12),
    new Sunny(temp: 35),
    new Cloudy(uvIndex: 6)
]

與 Java 的差異

  • non-sealed 關鍵字(或 @NonSealed 註解)不需要用來表示子類別開放延伸。未來版本的 Codenarc 可能有一個規則,允許希望遵循 Java 實務的 Groovy 開發人員依其意願。話雖如此,保持延伸限制(使用 finalsealed)將導致更多地方的未來類型檢查可以檢查類型的窮舉使用(例如 switch 表達式)。

  • Groovy 使用 @Sealed 註解來支援 JDK8+ 的密封類別。這些稱為模擬密封類別。此類類別將被 Groovy 編譯器識別為密封,但 Java 編譯器不會。對於 JDK17+,Groovy 會將密封類別資訊寫入位元組碼。這些稱為原生密封類別。請參閱 @SealedOptions 註解,以進一步控制是否建立模擬或原生密封類別。

  • Java 對密封階層中的類別有要求,它們必須在同一個模組或同一個套件中。Groovy 目前不強制執行此要求,但未來版本可能會執行。特別是,原生密封類別(請參閱前一點)可能會需要此要求。

注意
提示:密封類別可以使用 sealed(和相關)關鍵字或 @Sealed(和相關)註解。關鍵字樣式通常比較簡潔,但是如果你有一個編輯器或其他工具尚未提供對新關鍵字的支援,你可能比較喜歡註解樣式。用於撰寫密封類別的語法不會影響建立原生模擬密封類別。這完全由位元組碼版本和 @SealedOptions 中提供的選項決定。請注意,在使用關鍵字樣式定義的類別上使用 @SealedOptions 註解是沒問題的。
注意
警告:密封類別是一個孵化中的功能。雖然我們不預期會有大的變動,但一些小細節可能會在未來的 Groovy 版本中變動。

記錄和類記錄類別(孵化中)

Java 14 和 15 將記錄作為預覽功能引入,而 Java 16 中的記錄已從預覽狀態畢業。根據這篇記錄焦點文章,記錄「以較少的儀式對純粹資料聚合建模」。

Groovy 有 @Immutable@Canonical AST 轉換等功能,這些功能已經支援以較少的儀式對資料聚合建模,而這些功能在某種程度上與記錄的設計重疊,但它們並非直接等效。記錄最接近 @Immutable,並在組合中加入了一些變化。

Groovy 4 為 JDK16+ 新增了對原生記錄的支援,也為較早的 JDK 新增了類記錄類別(也稱為模擬記錄)。類記錄類別具有原生記錄的所有功能,但在位元組碼層級沒有與原生記錄相同資訊,因此 Java 編譯器在跨語言整合情境中不會將它們識別為記錄。請參閱 @RecordOptions 註解,以進一步控制是否建立模擬原生記錄。

類記錄類別看起來有點類似於使用 Groovy 的 @Immutable AST 轉換時產生的類別。該轉換本身是一個元註解(也稱為註解收集器),它結合了更細緻的功能。提供這些功能的類記錄重新組合相對簡單,這就是 Groovy 4 在其記錄實作中提供的功能。

您可以撰寫記錄定義如下

record Cyclist(String firstName, String lastName) { }

或使用此較長的格式(或多或少與上述單行定義轉換成相同內容)

@groovy.transform.RecordType
class Cyclist {
    String firstName
    String lastName
}

您會使用以下範例

def richie = new Cyclist('Richie', 'Porte')

這會產生具有以下特徵的類別

  • 它是隱含的最終版本

  • 它有一個私有最終欄位 firstName,並有一個存取器方法 firstName()lastName 亦同

  • 它有一個預設的 Cyclist(String, String) 建構函式

  • 它有一個預設的 serialVersionUID 為 0L

  • 它有隱含的 toString()equals()hashCode() 方法

@RecordType 註解結合了以下轉換/標記註解

@RecordBase
@RecordOptions
@TupleConstructor(namedVariant = true, force = true, defaultsMode = DefaultsMode.AUTO)
@PropertyOptions
@KnownImmutable
@POJO
@CompileStatic

RecordBase 註解也提供了 @ToString@EqualsAndHashCode 功能,委派給這些轉換或提供特殊的原生記錄等效項。

我們熱切期待進一步的回饋,了解 Groovy 使用者如何使用記錄或類記錄結構。

注意
提示:您可以使用 record 關鍵字或 @RecordType 註解,用於原生模擬記錄。關鍵字樣式通常比較簡潔,但是如果您有一個編輯器或其他工具尚未提供對新關鍵字和簡潔語法的支援,您可能比較喜歡註解樣式。用於撰寫記錄的語法不會影響建立原生模擬記錄。這完全由位元組碼版本和 @RecordOptions 中提供的選項決定。請注意,在使用關鍵字樣式定義的記錄上使用 @RecordOptions 註解是可以的。
注意
警告:記錄是一個孵化中的功能。雖然我們不預期會有大的變動,但一些小細節可能會在未來的 Groovy 版本中變更。

內建類型檢查器

Groovy 的靜態特性包含一個可擴充的類型檢查機制。此機制允許使用者

  • 選擇性地弱化類型檢查,以允許更多動態樣式代碼解析靜態檢查,或

  • 加強類型檢查,讓 Groovy 在需要時比 Java 嚴格許多

到目前為止,我們知道這項功能已在公司內部使用(例如類型檢查的 DSL),但我們尚未看到類型檢查器擴充套件的廣泛分享。從 Groovy 4 開始,我們將一些精選的類型檢查器組合在可選擇的 groovy-typecheckers 模組中,以鼓勵進一步使用此功能。

第一個包含項目是正規表示式的檢查器。考慮以下代碼

def newYearsEve = '2020-12-31'
def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/

這通過編譯,但由於我們「不小心」遺漏最後的右括號,因此在執行時會因 PatternSyntaxException 而失敗。我們可以使用新的檢查器在編譯時取得此回饋,如下所示

import groovy.transform.TypeChecked

@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')
def whenIs2020Over() {
    def newYearsEve = '2020-12-31'
    def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/
}

這會產生預期的編譯錯誤

1 compilation error:
[Static type checking] - Bad regex: Unclosed group near index 26
(\d{4})-(\d{1,2})-(\d{1,2}
 at line: 6, column: 19

與往常一樣,Groovy 的編譯器自訂機制允許您簡化此類檢查器的應用,例如僅舉一個範例,使用編譯器設定檔指令碼使其適用於全域。

我們歡迎進一步回饋,以納入 Groovy 中的其他類型檢查器擴充套件。

內建巨集方法

Groovy 巨集於 Groovy 2.5 中引入,以簡化建立 AST 轉換和其他處理編譯器 AST 資料結構的代碼。巨集的一部分,稱為巨集方法,允許在編譯期間將看起來像全域方法呼叫的內容替換為轉換的代碼。

與類型檢查器擴充套件有點類似,我們知道這項功能已在許多地方使用,但到目前為止,我們尚未看到巨集方法的廣泛分享。從 Groovy 4 開始,我們將一些精選的巨集方法組合在可選擇的 groovy-macro-library 模組中,以鼓勵進一步使用此功能。

第一個包含項目協助舊式除錯(窮人的序列化?)。假設您在編碼期間已定義許多變數

def num = 42
def list = [1 ,2, 3]
def range = 0..5
def string = 'foo'

假設現在您想要將這些列印出來以進行除錯。您可以撰寫一些適當的 println 陳述式,並可能加入一些呼叫 format() 的指令。您甚至可以使用 IDE 來協助您執行此操作。或者,SVNV` 巨集方法可以提供協助。

SV 巨集方法會建立一個字串(實際上是 gapi:groovy.lang.GString),其中包含變數名稱和值。以下是一個範例

println SV(num, list, range, string)

輸出

num=42, list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo

在此,SV 巨集方法會在編譯過程中立即執行。編譯器會將明顯的 SV 全域方法呼叫替換為一個運算式,該運算式會結合所提供變數的名稱和 toString() 值。

還有另外兩種變化。SVI 會呼叫 Groovy 的 inspect() 方法,而不是 toString(),而 SVD 會呼叫 Groovy 的 dump() 方法。因此,此程式碼

println SVI(range)

會產生下列輸出

range=0..5

而此程式碼

println SVD(range)

會產生

range=<groovy.lang.IntRange@14 from=0 to=5 reverse=false inclusiveRight=true inclusiveLeft=true modCount=0>

NV 巨集方法提供類似於 SV 的功能,但它不會建立「字串」,而是建立 gapi:groovy.lang.NamedValue,讓您可以進一步處理名稱和值資訊。以下是一個範例

def r = NV(range)
assert r instanceof NamedValue
assert r.name == 'range' && r.val == 0..5

還有一個 NVL 巨集方法,它會建立一個 NamedValue 執行個體清單。

def nsl = NVL(num, string)
assert nsl*.name == ['num', 'string']
assert nsl*.val == [42, 'foo']

我們歡迎進一步回饋,以納入 Groovy 中的其他巨集方法。如果您啟用此選用模組,但想要限制啟用的巨集方法,現在有一個機制可以停用個別巨集方法(和延伸方法)GROOVY-9675

JavaShell(孵化中)

GroovyShell 的 Java 等效項,讓您可以更輕鬆地使用 Java 程式碼片段。例如,以下片段顯示編譯一個 記錄(JDK14)並使用 Groovy 檢查其 toString

import org.apache.groovy.util.JavaShell
def opts = ['--enable-preview', '--release', '14']
def src = 'record Coord(int x, int y) {}'
Class coordClass = new JavaShell().compile('Coord', opts, src)
assert coordClass.newInstance(5, 10).toString() == 'Coord[x=5, y=10]'

此功能在 Groovy 程式碼庫中的許多地方用於測試目的。各種程式碼片段會使用 Java 和 Groovy 編譯,以確保編譯器按預期運作。我們也使用此功能為多語開發人員提供生產力提升,讓他們可以在 Groovy Console 中編譯和/或執行 Java 程式碼(作為 Java)

image

POJO 註解(孵化中)

Groovy 支援動態和靜態性質。動態 Groovy 的強大功能和彈性來自於(可能廣泛)使用執行時期。靜態 Groovy 對執行時期函式庫的依賴較少。許多方法呼叫會有對應於直接 JVM 方法呼叫的位元組碼(類似於 Java 位元組碼),而 Groovy 執行時期通常會完全被略過。但是,即使對於靜態 Groovy,與 Groovy jar 的硬連結仍然存在。所有 Groovy 類別仍會實作 GroovyObject 介面(因此具有 getMetaClassinvokeMethod 等方法),而且還有一些其他會呼叫 Groovy 執行時期的地方。

@POJO 標記介面用於表示產生的類別更像是純粹的 Java 物件,而非增強的 Groovy 物件。目前會忽略此註解,除非與 @CompileStatic 結合使用。對於此類類別,編譯器不會產生 Groovy 通常需要的函式,例如 getMetaClass()。此功能通常用於產生需要與 Java 或 Java 架構搭配使用的類別,在 Java 可能因 Groovy 的「管線」函式而混淆的情況下。

此功能正在孵化中。目前,註解的存在應視為對編譯器的提示,如果可以,產生不依賴 Groovy 執行時期的位元組碼,但並非保證

@CompileStatic 的使用者會知道,當他們切換到靜態 Groovy 時,某些動態功能將無法使用。他們可能會預期使用 @CompileStatic@POJO 可能會產生更多限制。這並非完全正確。加入 @POJO 的確會在某些地方產生更多類似 Java 的程式碼,但許多 Groovy 功能仍然有效。

考慮以下範例。首先是 Groovy Point 類別

@CompileStatic
@POJO
@Canonical(includeNames = true)
class Point {
    Integer x, y
}

現在是 Groovy PointList 類別

@CompileStatic
@POJO
class PointList {
    @Delegate
    List<Point> points
}

我們可以使用 groovyc 以正常方式編譯這些類別,並應看到產生的預期 Point.classPointList.class 檔案。

然後,我們可以編譯以下 Java 程式碼。我們不需要讓 javacjava 使用 Groovy jar,我們只需要從前一步驟產生的類別檔案。

Predicate<Point> xNeqY = p -> p.getX() != p.getY();  // (1)

Point p13 = new Point(1, 3);
List<Point> pts = List.of(p13, new Point(2, 2), new Point(3, 1));
PointList list = new PointList();
list.setPoints(pts);

System.out.println(list.size());
System.out.println(list.contains(p13));

list.forEach(System.out::println);

long count = list.stream().filter(xNeqY).collect(counting());  // (2)
System.out.println(count);
  1. 檢查 x 是否不等於 y

  2. 計算 x 不等於 y 的點數

請注意,儘管我們的 PointList 類別有許多可用的清單函式(sizecontainsforEachstream 等),這是拜 Groovy 的 @Delegate 轉換所賜,這些函式已內嵌到類別檔案中,而產生的位元組碼不會呼叫任何 Groovy 函式庫或依賴任何執行時期程式碼。

執行時,會產生以下輸出

3
true
Point(x:1, y:3)
Point(x:2, y:2)
Point(x:3, y:1)
2

實質上,這開啟了將 Groovy 用作類似於 Lombok 的預處理器的可能性,但由 Groovy 語言支援。

注意
警告:並非編譯器的所有部分和 AST 轉換都已認識 POJO。使用此方法是否需要 Groovy jar 位於類別路徑中,可能因情況而異。雖然我們預期隨著時間推移會有一些改進,允許更多 Groovy 結構與 @POJO 搭配使用,但我們目前無法保證最終會支援所有結構。因此,此功能仍處於孵化階段。

Groovy 合約(孵化中)

此選用模組支援契約式程式設計風格。更具體來說,它提供契約註解,支援在 Groovy 類別和介面上指定類別不變式、前置條件和後置條件。以下是範例

import groovy.contracts.*

@Invariant({ speed() >= 0 })
class Rocket {
    int speed = 0
    boolean started = true

    @Requires({ isStarted() })
    @Ensures({ old.speed < speed })
    def accelerate(inc) { speed += inc }

    def isStarted() { started }

    def speed() { speed }
}

def r = new Rocket()
r.accelerate(5)

這會導致檢查邏輯(與契約宣告相符)會依需要注入類別方法和建構函式中。檢查邏輯會確保在方法執行前滿足任何前置條件,在方法執行後任何後置條件成立,以及在方法呼叫前後任何類別不變式為真。

此模組取代先前外部的 gcontracts 專案,該專案現已封存。

GINQ,又稱 Groovy 整合式查詢或 GQuery(孵化中)

GQuery 支援以類似 SQL 的樣式查詢集合。這可能涉及清單和/或對應,或您自己的網域物件,或在處理 JSON、XML 和其他結構化資料時回傳的物件。

from p in persons
leftjoin c in cities on p.city.name == c.name
where c.name == 'Shanghai'
select p.name, c.name as cityName

from p in persons
groupby p.gender
having p.gender == 'Male'
select p.gender, max(p.age)

from p in persons
orderby p.age in desc, p.name
select p.name

from n in numbers
where n > 0 && n <= 3
select n * 2

from n1 in nums1
innerjoin n2 in nums2 on n1 == n2
select n1 + 1, n2

我們來看一個完整的範例。假設我們有關於水果、其價格(每 100 公克)和維生素 C 濃度(每 100 公克)的 JSON 格式資訊。我們可以如下處理 JSON

import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText('''
{
    "prices": [
        {"name": "Kakuda plum",      "price": 13},
        {"name": "Camu camu",        "price": 25},
        {"name": "Acerola cherries", "price": 39},
        {"name": "Guava",            "price": 2.5},
        {"name": "Kiwifruit",        "price": 0.4},
        {"name": "Orange",           "price": 0.4}
    ],
    "vitC": [
        {"name": "Kakuda plum",      "conc": 5300},
        {"name": "Camu camu",        "conc": 2800},
        {"name": "Acerola cherries", "conc": 1677},
        {"name": "Guava",            "conc": 228},
        {"name": "Kiwifruit",        "conc": 144},
        {"name": "Orange",           "conc": 53}
    ]
}
''')

現在,假設我們預算有限,想要選擇最具成本效益的水果來購買,以幫助我們達到每日維生素 C 需求。我們會將 價格維生素 C 資訊聯結,並依最具成本效益的水果排序。我們會選擇前 2 名,以防我們去購物時第一選擇缺貨。我們可以看出,對於此資料,卡卡杜李其次是奇異果是我們的最佳選擇

assert GQ {
    from p in json.prices
    join c in json.vitC on c.name == p.name
    orderby c.conc / p.price in desc
    limit 2
    select p.name
}.toList() == ['Kakuda plum', 'Kiwifruit']

我們可以再次查看 XML 的相同範例。我們的 XML 處理程式碼可能類似以下

import groovy.xml.XmlSlurper
def root = new XmlSlurper().parseText('''
<root>
    <prices>
        <price name="Kakuda plum">13</price>
        <price name="Camu camu">25</price>
        <price name="Acerola cherries">39</price>
        <price name="Guava">2.5</price>
        <price name="Kiwifruit">0.4</price>
        <price name="Orange">0.4</price>
    </prices>
    <vitaminC>
        <conc name="Kakuda plum">5300</conc>
        <conc name="Camu camuum">2800</conc>
        <conc name="Acerola cherries">1677</conc>
        <conc name="Guava">228</conc>
        <conc name="Kiwifruit">144</conc>
        <conc name="Orange">53</conc>
    </vitaminC>
</root>
''')

我們的 GQuery 程式碼可能類似以下

assert GQ {
    from p in root.prices.price
    join c in root.vitaminC.conc on c.@name == p.@name
    orderby c.toInteger() / p.toDouble() in desc
    limit 2
    select p.@name
}.toList() == ['Kakuda plum', 'Kiwifruit']

在未來的 Groovy 版本中,我們希望提供 GQuery 支援,以供 SQL 資料庫使用,其中會根據 GQuery 表達式產生最佳化的 SQL 查詢,非常類似 Groovy 的 DataSet 功能。在此同時,對於少量資料,您可以使用 Groovy 的標準 SQL 功能,它會將來自資料庫的查詢回傳為集合。以下是資料庫的程式碼範例,其中 PriceVitaminC 表格都有 nameper100g 欄位

// ... create sql connection ...
def price = sql.rows('SELECT * FROM Price')
def vitC = sql.rows('SELECT * FROM VitaminC')
assert GQ {
    from p in price
    join c in vitC on c.name == p.name
    orderby c.per100g / p.per100g in desc
    limit 2
    select p.name
}.toList() == ['Kakuda plum', 'Kiwifruit']
// ... close connection ...

可以在 GQuery 文件(或直接在 原始程式碼存放庫)中找到更多範例。

TOML 支援(孵化中)

現在提供支援來處理 TOML 為基礎的檔案,包括建置

def builder = new TomlBuilder()
builder.records {
    car {
        name 'HSV Maloo'
        make 'Holden'
        year 2006
        country 'Australia'
        homepage new URL('http://example.org')
        record {
            type 'speed'
            description 'production pickup truck with speed of 271kph'
        }
    }
}

和剖析

def ts = new TomlSlurper()
def toml = ts.parseText(builder.toString())

assert 'HSV Maloo' == toml.records.car.name
assert 'Holden' == toml.records.car.make
assert 2006 == toml.records.car.year
assert 'Australia' == toml.records.car.country
assert 'http://example.org' == toml.records.car.homepage
assert 'speed' == toml.records.car.record.type
assert 'production pickup truck with speed of 271kph' == toml.records.car.record.description

更多範例可以在 groovy-toml 文件中找到。

其他改進

GString 效能改善

GString 內部結構已改進以提升效能。在安全的情況下,GString toString 值現在會自動快取。雖然不常使用,但 GString 允許檢視(甚至變更!)其內部資料結構。在這種情況下,快取會停用。如果您想要檢視但不變更內部資料結構,您可以在 GStringImpl 中呼叫 freeze() 方法,以禁止變更內部資料結構,讓快取保持作用。 GROOVY-9637

例如,以下指令碼使用 Groovy 3 執行約 10 秒,使用 Groovy 4 執行約 0.1 秒

def now = java.time.LocalDateTime.now()
def gs = "integer: ${1}, double: ${1.2d}, string: ${'x'}, class: ${Map.class}, boolean: ${true}, date: ${now}"
long b = System.currentTimeMillis()
for (int i = 0; i < 10000000; i++) {
    gs.toString()
}
long e = System.currentTimeMillis()
println "${e - b}ms"

增強的範圍

Groovy 一直支援包含式範圍,例如 3..5,以及排除式範圍(或右開),例如 4..<10。從 Groovy 4 開始,範圍可以是封閉的、左開的,例如 3<..5,右開的或兩側都開的,例如 0<..<3。此類範圍會排除最左邊或最右邊的值。 GROOVY-9649

支援沒有前導零的十進位分數文字

Groovy 以前需要在分數值中加上前導零,但現在也支援省略前導零。

def half = .5
def otherHalf = 0.5  // leading zero remains supported
double third = .333d
float quarter = .25f
def fractions = [.1, .2, .3]

// can be used for ranges too (with a rare edge case you might want to avoid)
def range1 = -1.5..<.5    // okay here
def range2 = -1.5.. .5    // space is okay but harder for humans (1)
def range3 = -1.5..0.5    // leading zero edge case (1)
assert range3 == [-1.5, -.5, .5]
  1. 沒有前導零的分數值不能緊接在範圍 .. 算子之後。三個連續的點會令人混淆,而且類似於變數參數表示法。您應該留一個空格(對人類讀者來說可能仍然令人混淆)或保留前導零(建議採用)。

JSR308 改進(孵化中)

Groovy 在最近的版本中一直改進 JSR-308 支援。在 Groovy 4.0 中,新增了其他支援。特別是,現在支援泛型型別上的型別註解。這對 Jqwik 屬性測試函式庫和 Bean Validation 2 架構等工具的使用者來說很有用。以下是 Jqwik 測試的範例

@Grab('net.jqwik:jqwik:1.5.5')
import net.jqwik.api.*
import net.jqwik.api.constraints.*

class PropertyBasedTests {
    @Property
    def uniqueInList(@ForAll @Size(5) @UniqueElements List<@IntRange(min = 0, max = 10) Integer> aList) {
        assert aList.size() == aList.toSet().size()
        assert aList.every{ anInt -> anInt >= 0 && anInt <= 10 }
    }
}

在 Groovy 的早期版本中,已處理 @Forall@Size@UniqueElements 註解,但 List 泛型型別上的 @IntRange 註解未出現在已產生的位元組碼中,現在則會出現。

以下是 Bean Validation 2 架構的範例

@Grab('org.hibernate.validator:hibernate-validator:7.0.1.Final')
@Grab('org.hibernate.validator:hibernate-validator-cdi:7.0.1.Final')
@Grab('org.glassfish:jakarta.el:4.0.0')
import jakarta.validation.constraints.*
import jakarta.validation.*
import groovy.transform.*

@Canonical
class Car {
    @NotNull @Size(min = 2, max = 14) String make
    @Min(1L) int seats
    List<@NotBlank String> owners
}

def validator = Validation.buildDefaultValidatorFactory().validator

def violations = validator.validate(new Car(make: 'T', seats: 1))
assert violations*.message == ['size must be between 2 and 14']

violations = validator.validate(new Car(make: 'Tesla', owners: ['']))
assert violations*.message.toSet() == ['must be greater than or equal to 1', 'must not be blank'] as Set

violations = validator.validate(new Car(make: 'Tesla', owners: ['Elon'], seats: 2))
assert !violations

同樣地,除了 List 泛型型別上的 @NonBlank 註解之外,以前支援所有註解,現在 @NonBlank 也會出現在位元組碼中。

這項功能標示為孵化中。已產生的位元組碼預計不會變更,但在功能離開孵化中狀態之前,註解在編譯期間的 AST 表示法的某些小細節可能會略有變更。

此外,出現在程式碼中的型別註解(例如,區域變數型別、強制轉換表達式型別、捕捉區塊例外情況型別)仍是進行中的工作。

AST 轉換優先順序

AST 轉換處理的順序首先由轉換的 @GroovyASTTransformation 宣告中所宣告的 phase 決定。對於宣告為在相同階段的轉換,接著會使用相關轉換註解在原始碼中出現的順序。

現在,轉換撰寫者也可以為其轉換指定優先順序。為此,AST 轉換必須實作 TransformWithPriority 介面,並在實作的 priority() 方法中以整數回傳其優先順序。預設優先順序為 0。具有最高正優先順序的轉換將會首先處理。負優先順序將在所有優先順序為零(預設)的轉換之後處理。

請注意,轉換仍會一起處理。優先順序只會影響其他轉換之間的排序。各編譯階段的其他部分保持不變。

舊版整合

舊解析器移除

Groovy 3 導入了新的「Parrot」解析器,它支援 lambda、方法參考和許多其他調整。在 Groovy 3 中,您仍然可以改回舊解析器(如果您想要的話)。在 Groovy 4 中,基於 Antlr2 的舊解析器已移除。如果您需要舊解析器,請使用舊版本的 Groovy。

傳統位元組碼產生移除

在許多版本中,Groovy 可以產生傳統的 基於呼叫站點 位元組碼或針對 JDK7+ 呼叫動態(「indy」)位元組碼指令的位元組碼。您可以使用編譯器開關在它們之間切換,而且我們建置了兩組 jar(「一般」和「-indy」),分別啟用和停用開關。在 Groovy 4.0 中,只能產生使用後者方法的位元組碼。現在有一組 jar,而且它們剛好是 indy 風味的。

目前,Groovy 執行時期仍然包含對使用舊版本 Groovy 編譯的類別的任何必要支援。如果您需要產生舊式位元組碼,請使用 3.x 之前的 Groovy 版本。

這項工作原本計畫在 Groovy 3.0 中進行,但有許多地方的「indy」程式碼明顯比「傳統」位元組碼慢。我們已進行許多速度改善(從 GROOVY-8298 開始),並具備調整內部閾值的某些能力(在程式碼庫中搜尋 groovy.indy.optimize.thresholdgroovy.indy.fallback.threshold)。這項工作為我們帶來了有用的速度改善,但我們歡迎進一步的回饋,以協助改善 indy 位元組碼的整體效能。

其他重大變更

  • Groovy 在其選用的 groovy-jaxb 模組中使用 JAXB 技術 時,新增了一些極小的增強功能。由於 JAXB 不再包含在 JDK 中,我們移除了這個模組。想要使用該功能的使用者可能會使用 Groovy 4 的該模組的 Groovy 3 版本,儘管我們不保證未來會這樣做。(GROOVY-10005)。

  • 選用的 groovy-bsf 模組提供 Groovy BSF 引擎,用於 BSF(又稱 beanshell)架構的版本 2。此版本自 2005 年以來沒有發布,且已達到生命週期結束。在 Groovy 4 中,我們已移除此模組。想要使用該功能的使用者可能會使用 Groovy 4 的該模組的 Groovy 3 版本,儘管我們不保證未來會這樣做。(GROOVY-10023)。

  • 許多類別先前「洩露」ASM 常數,而這些常數本質上是透過實作 `Opcodes` 介面而產生的內部實作細節。這通常不會影響大多數的 Groovy 程式碼,但可能會影響處理 AST 節點的程式碼,例如 AST 轉換。在使用 Groovy 4 編譯之前,其中一些程式碼可能需要新增一個或多個適當的靜態匯入陳述式。延伸 `AbstractASTTransformation` 的 AST 轉換就是一個潛在受影響類別的範例。(GROOVY-9736

  • ASTTest 先前具有 `RUNTIME` 保留,但現在具有 `SOURCE` 保留。我們不知道有任何使用者使用舊保留,但知道有各種問題會保留舊值。 GROOVY-9702

  • Groovy 的 `intersect` DGM 方法在提供投影封閉/比較器時,與其他語言具有不同的語意。其他語言在這種情況下通常具有 `intersectBy` 方法,而不是像 Groovy 那樣覆載 `intersect` 算子。當沒有投影函數在執行時,`a.intersect(b)` 應始終等於 `b.intersect(a)`。當投影函數在執行時,大多數語言將 `a.intersect(b)` 定義為 `a` 中元素的子集,當投影時與 `b` 中的投影值相符。因此,結果值總是從 `a` 中取得。可以反轉所涉及的物件,以從 `b` 中取得元素。Groovy 的語意過去與大多數其他語言相反,但現在已調整一致。以下是一些具有新行為的範例

    def abs = { a, b -> a.abs() <=> b.abs() }
    assert [1, 2].intersect([-2, -3], abs) == [2]
    assert [-2, -3].intersect([1, 2], abs) == [-2]
    
    def round = { a, b -> a.round() <=> b.round() }
    assert [1.1, 2.2].intersect([2.5, 3.5], round) == [2.2]
    assert [2.5, 3.5].intersect([1.1, 2.2], round) == [2.5]

    只要反轉物件的順序,就能取得先前的行為,例如使用 `foo.intersect(bar)` 而不是 `bar.intersect(foo)`。 GROOVY-10275

  • 各種邊界案例的 JavaBean 屬性命名慣例有一些不一致之處,例如對於名稱為單一大寫 `X` 且具有 `getX` 存取器的欄位,則欄位優先於存取器。 GROOVY-9618

  • 許多大多數是內部資料結構類別,例如 AbstractConcurrentMapBase、AbstractConcurrentMap、ManagedConcurrentMap 已棄用,且其用法已替換為更好的替代方案。這應該大多數是看不見的,但有些變更可能會影響直接使用內部 Groovy 類別的使用者。 GROOVY-9631

  • 我們升級了 Picocli 版本。這導致一些 CLI 說明訊息的次要格式變更。我們建議不要依賴這些訊息的確切格式。 GROOVY-9627

  • 我們目前正嘗試改進 Groovy 程式碼在某些情況下存取私有欄位的方式,在這些情況下,預期會存取這些欄位,但會有問題,例如在涉及子類別或內部類別的閉包定義中 (GROOVY-5438)。在這種情況下,你可能會注意到 Groovy 4 程式碼中會中斷,直到這個問題進展為止。在此期間,你可以使用閉包外部的局部變數來參照相關欄位,然後在閉包中參照這些局部變數,作為一種解決方法。

  • 較早的 Groovy 版本意外地將常數 -0.0f 和 -0.0d 儲存為分別與 0.0f 和 0.0d 相同。這只適用於明確的常數,也就是說,它不適用於導致正或負零的計算。這也表示,在某些情況下,正負零的比較會傳回 true,而這些情況下它們應該不同,而且呼叫 unique 可能會導致只包含正零的集合,而不是正負零(根據 IEEE-745 的正確答案)。你可能會或可能不會受到影響,這取決於你使用的是原始或包裝浮點變數。如果你受到影響,而且想要舊有的行為,請考慮使用 equalsIgnoreZeroSign 和布林 ignoreZeroSign 建構函變數到 NumberAwareComparator。這些修改也已回溯移植到 Groovy 3,因此請考慮在 Groovy 3 程式碼中使用它們,而不是依賴舊有的行為,以便你的程式碼可以在各版本中正確執行。修正本身並未回溯移植,以避免中斷依賴於意外有缺陷行為的現有程式碼。
    錯誤修正:GROOVY-9797
    改善的文件和輔助方法:GROOVY-9981

  • 各種 Groovy 測試類別對 JUnit 3/4 類別有不需要的隱藏相依性。修改後,現在可以在類別路徑上沒有 Junit 3/4 的情況下使用這些類別,例如 JUnit 5(或 Spock)。只有當程式碼明確查看拋出例外狀況的類別或透過反射檢查類別階層時,這才會是一個中斷變更。
    NotYetImplementedGROOVY-9492
    GroovyAssertGROOVY-9767

  • 多個 Sql#call 變體錯誤地擲出 Exception 而不是 SQLException。這是一個二進位元中斷變更。編譯依賴於這些方法的程式碼時應小心,使用較舊版本的 Groovy,然後在 Groovy 4 上執行,反之亦然。 GROOVY-9923

  • 我們從公開 API 中移除 StaticTypeCheckingVisitor#collectAllInterfacesByName,因為它有錯誤,而且有許多可用的替代方案。我們不知道有任何架構使用這個方法。儘管它是公開的,但它主要被視為內部。 GROOVY-10123

  • 兩個 jar 檔案 (servlet-api.jarjsp-api.jar) 理論上是「提供的」依賴項,但之前已複製到 Groovy 二進位元發行版中。現在不再是這樣。 GROOVY-9827

  • 涉及陣列上 plus 的 Groovy 程式碼在某些情況下中斷了參考透明度。表達式 b + c,其中 bc 是陣列,在兩個表達式 a = b + cb = b + c 中可能會產生不同的結果。後一個表達式 (b += c 的簡寫) 是類型保留,但前一個表達式則傳回 Object[]。類型保留的行為是預期的。 GROOVY-6837

    提示

    若要模擬舊行為:如果 b 不是 Object 陣列,而你想要 Object 陣列結果,則不要使用 b + c,而是使用下列其中一個

    • b.union(c)

    • new Object[0] + b + c

    • [] as Object[] + b + c

  • Groovy 的語法從 Eiffel 程式語言借用了「資訊隱藏原則」的想法,藉此存取公開欄位或屬性 (具有 getter 的私有欄位) 可以具有相同的語法形式。這個想法沒有傳遞到物件元類別中的 getProperties() 方法。現在 getProperties() 也會傳回公開欄位。 GROOVY-10449

JDK 需求

Groovy 4.0 需要 JDK16+ 來建置,而 JDK8 是我們支援的 JRE 的最低版本。Groovy 已在 JDK 版本 8 到 17 上進行測試。

更多資訊

4.0.2 的附錄

  • Groovy 4 使用 Gradle 的模組元資料功能,加強了其相依項所儲存的元資料。作為此變更的一部分,存取 groovy-all 相依項的方式發生了改變,這讓許多使用者感到困惑。特別是,現在需要使用以前不需要的 platform。模組元資料已獲得改善,現在不再需要使用 platformGROOVY-10543

  • 已新增初步的 JDK19 支援

  • Groovy 選擇性地支援使用安全性原則檔案,在執行未允許的操作(例如讀取屬性、退出 JVM 或存取檔案等資源)時觸發安全性例外狀況。隨著 Java 計畫逐步淘汰安全性原則架構 (JEP-411),未來的 Groovy 版本可能會逐步淘汰此選擇性支援。在此同時,如果使用此類功能,使用者可能會看到警告訊息,並可能在 JDK 18 或 19 中看到例外狀況。

  • 特別要注意安全性例外狀況(請參閱前一點),在 JDK18 或 JDK19 上使用 groovysh 時,使用者應將 JAVA_OPTS 設定為 -Djava.security.manager=allowgroovysh 工具使用安全性管理員禁止呼叫 System::exit。預計將在某個時間點出現處理此情境的替代 API,而 groovysh 將在可用的時候轉移到這些 API。