與 Java 的差異

Groovy 盡可能地對 Java 開發人員來說是自然的。在設計 Groovy 時,我們試著遵循最小的驚喜原則,特別是對於來自 Java 背景的 Groovy 學習者。

以下是 Java 和 Groovy 之間的所有主要差異。

1. 預設匯入

所有這些套件和類別都是預設匯入的,也就是說您不必使用明確的 import 陳述式來使用它們

  • java.io.*

  • java.lang.*

  • java.math.BigDecimal

  • java.math.BigInteger

  • java.net.*

  • java.util.*

  • groovy.lang.*

  • groovy.util.*

2. 多重方法

在 Groovy 中,將在執行階段選擇要呼叫的方法。這稱為執行階段調度或多重方法。這表示將根據執行階段參數的類型來選擇方法。在 Java 中,情況相反:方法是在編譯階段根據宣告的類型來選擇的。

以下程式碼寫成 Java 程式碼後,可以在 Java 和 Groovy 中編譯,但行為會不同

int method(String arg) {
    return 1;
}
int method(Object arg) {
    return 2;
}
Object o = "Object";
int result = method(o);

在 Java 中,您將擁有

assertEquals(2, result);

而在 Groovy 中

assertEquals(1, result);

這是因為 Java 將使用靜態資訊類型,即 o 被宣告為 Object,而 Groovy 會在執行時期選擇,也就是在實際呼叫方法時。由於它使用 String 呼叫,因此會呼叫 String 版本。

3. 陣列初始化項

在 Java 中,陣列初始化項採用以下兩種形式

int[] array = {1, 2, 3};             // Java array initializer shorthand syntax
int[] array2 = new int[] {4, 5, 6};  // Java array initializer long syntax

在 Groovy 中,{ …​ } 區塊保留給閉包。這表示您無法使用 Java 的陣列初始化項簡寫語法建立陣列文字。您改用 Groovy 的文字清單表示法,如下所示

int[] array = [1, 2, 3]

對於 Groovy 3+,您可以選擇使用 Java 的陣列初始化項長語法

def array2 = new int[] {1, 2, 3} // Groovy 3.0+ supports the Java-style array initialization long syntax

4. 套件範圍可見性

在 Groovy 中,省略欄位上的修飾詞不會產生像 Java 中的套件私有欄位

class Person {
    String name
}

相反地,它用於建立屬性,也就是說私有欄位、相關的取得器和相關的設定器

您可以透過使用 @PackageScope 註解來建立套件私有欄位

class Person {
    @PackageScope String name
}

5. ARM 區塊

Java 7 引入了 ARM(自動資源管理)區塊(也稱為 try-with-resources)區塊,如下所示

Path file = Paths.get("/path/to/file");
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

} catch (IOException e) {
    e.printStackTrace();
}

Groovy 3+ 支援此類區塊。然而,Groovy 提供了依賴於閉包的各種方法,這些方法具有相同的效應,同時更符合慣例。例如

new File('/path/to/file').eachLine('UTF-8') {
   println it
}

或者,如果您想要更接近 Java 的版本

new File('/path/to/file').withReader('UTF-8') { reader ->
   reader.eachLine {
       println it
   }
}

6. 內部類別

匿名內部類別和巢狀類別的實作緊密遵循 Java,但有一些差異,例如從此類類別內部存取的局部變數不必是 final。當產生內部類別位元組碼時,我們會利用我們用於 groovy.lang.Closure 的一些實作詳細資料。

6.1. 靜態內部類別

以下是靜態內部類別的範例

class A {
    static class B {}
}

new A.B()

靜態內部類別的使用是最受支援的。如果您絕對需要一個內部類別,您應該讓它成為一個靜態類別。

6.2. 匿名內部類別

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

CountDownLatch called = new CountDownLatch(1)

Timer timer = new Timer()
timer.schedule(new TimerTask() {
    void run() {
        called.countDown()
    }
}, 0)

assert called.await(10, TimeUnit.SECONDS)

6.3. 建立非靜態內部類別的實例

在 Java 中,您可以這樣做

public class Y {
    public class X {}
    public X foo() {
        return new X();
    }
    public static X createX(Y y) {
        return y.new X();
    }
}

在 3.0.0 之前,Groovy 不支援 y.new X() 語法。相反地,您必須撰寫 new X(y),如下面的程式碼所示

public class Y {
    public class X {}
    public X foo() {
        return new X()
    }
    public static X createX(Y y) {
        return new X(y)
    }
}
不過請注意,Groovy 支援呼叫具有單一參數的方法,而無需提供引數。然後,參數的值將為 null。基本上,呼叫建構函式適用相同的規則。例如,您可能會寫入 new X() 而不是 new X(this)。由於這也可能是常規方式,因此我們尚未找到一個好的方法來防止這個問題。
Groovy 3.0.0 支援 Java 風格語法來建立非靜態內部類別的實例。

7. Lambda 表達式和方法參考運算子

Java 8+ 支援 lambda 表達式和方法參考運算子 (::)

Runnable run = () -> System.out.println("Run");  // Java
list.forEach(System.out::println);

Groovy 3 以上版本也在 Parrot 解析器中支援這些功能。在早期版本的 Groovy 中,您應該改用閉包

Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)

8. GString

由於雙引號字串文字會被解釋為 GString 值,因此如果使用 Groovy 和 Java 編譯器編譯包含美元字元的 String 文字的類別,Groovy 可能會發生編譯錯誤或產生細微不同的程式碼。

雖然通常,如果 API 宣告參數的類型,Groovy 會在 GStringString 之間自動轉換,但請注意接受 Object 參數並檢查實際類型的 Java API。

9. 字串和字元文字

Groovy 中的單引號文字用於 String,而雙引號則會產生 StringGString,具體取決於文字中是否有內插。

assert 'c'.class == String
assert "c".class == String
assert "c${1}".class in GString

只有在指定給類型為 char 的變數時,Groovy 才會自動將單字元 String 轉換為 char。當呼叫具有 char 類型引數的方法時,我們需要明確轉換或確保該值已事先轉換。

char a = 'a'
assert Character.digit(a, 16) == 10: 'But Groovy does boxing'
assert Character.digit((char) 'a', 16) == 10

try {
  assert Character.digit('a', 16) == 10
  assert false: 'Need explicit cast'
} catch(MissingMethodException e) {
}

Groovy 支援兩種轉型風格,而轉型為 char 時,在轉型多字元字串時會有細微的差異。Groovy 風格的轉型較為寬鬆,會取用第一個字元,而 C 風格的轉型則會失敗並產生例外。

// for single char strings, both are the same
assert ((char) "c").class == Character
assert ("c" as char).class == Character

// for multi char strings they are not
try {
  ((char) 'cx') == 'c'
  assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

10. == 的行為

在 Java 中,== 表示基本型別的相等性或物件的身分。在 Groovy 中,== 在所有地方都表示相等性。對於非基本型別,在評估 Comparable 物件的相等性時,它會轉譯為 a.compareTo(b) == 0,否則會轉譯為 a.equals(b)

若要檢查身分(參考相等性),請使用 is 方法:a.is(b)。從 Groovy 3 開始,您也可以使用 === 算子(或否定版本):a === b(或 c !== d)。

11. 基本型別和包裝器

在純物件導向語言中,所有內容都會是物件。Java 採取的立場是,int、boolean 和 double 等基本型別使用頻率很高,值得特殊處理。基本型別可以有效地儲存和操作,但不能用於所有可以使用物件的場合。幸運的是,Java 在將基本型別傳遞為參數或用作回傳型別時,會自動封裝和拆封。

public class Main {           // Java

   float f1 = 1.0f;
   Float f2 = 2.0f;

   float add(Float a1, float a2) { return a1 + a2; }

   Float calc() { return add(f1, f2); } (1)

    public static void main(String[] args) {
       Float calcResult = new Main().calc();
       System.out.println(calcResult); // => 3.0
    }
}
1 add 方法預期包裝器然後是基本型別參數,但我們提供的是基本型別然後是包裝器型別的參數。類似地,add 的回傳型別是基本型別,但我們需要包裝器型別。

Groovy 也會執行相同的動作

class Main {

    float f1 = 1.0f
    Float f2 = 2.0f

    float add(Float a1, float a2) { a1 + a2 }

    Float calc() { add(f1, f2) }
}

assert new Main().calc() == 3.0

Groovy 也支援基本型別和物件型別,然而,它進一步推動了 OO 純粹性;它盡力將所有內容都視為物件。任何基本型別變數或欄位都可以像物件一樣處理,並且會在需要時自動封裝。雖然基本型別可能會在底層使用,但只要有可能,它們的使用應該與一般物件的使用沒有區別,並且會在需要時進行封裝/拆封。

以下是使用 Java 嘗試(對 Java 來說不正確地)取消參考基本型別 float 的一個小範例

public class Main {           // Java

    public float z1 = 0.0f;

    public static void main(String[] args){
      new Main().z1.equals(1.0f); // DOESN'T COMPILE, error: float cannot be dereferenced
    }
}

使用 Groovy 的相同範例會編譯並成功執行

class Main {
    float z1 = 0.0f
}
assert !(new Main().z1.equals(1.0f))

由於 Groovy 額外使用了封裝/拆封,因此它不會遵循 Java 的行為,即擴充優先於封裝。以下是使用 int 的範例

int i
m(i)

void m(long l) {           (1)
    println "in m(long)"
}

void m(Integer i) {        (2)
    println "in m(Integer)"
}
1 這是 Java 會呼叫的方法,因為擴充優先於拆封。
2 這是 Groovy 實際呼叫的方法,因為所有原始參考都使用其包裝類別。

11.1. 使用 @CompileStatic 進行數字原始最佳化

由於 Groovy 在更多地方轉換為包裝類別,您可能會懷疑它是否會為數字表達式產生效率較低的位元組碼。Groovy 有一組高度最佳化的類別,用於執行數學運算。使用 @CompileStatic 時,僅包含原始碼的表達式會使用 Java 會使用的相同位元組碼。

11.2. 正/負零邊界情況

Java float/double 運算(針對原始碼和包裝類別)遵循 IEEE 754 標準,但正零和負零之間有一個有趣的邊界情況。該標準支援區分這兩個情況,雖然在許多情況下,程式設計師可能不在乎差異,但在某些數學或資料科學情況下,區分是很重要的。

對於原始碼,當比較此類值時,Java 會對應到一個特殊的 位元組碼指令,其性質為「正零和負零被視為相等」。

jshell> float f1 = 0.0f
f1 ==> 0.0

jshell> float f2 = -0.0f
f2 ==> -0.0

jshell> f1 == f2
$3 ==> true

對於包裝類別,例如 java.base/java.lang.Float#equals(java.lang.Object),結果為 false,情況相同。

jshell> Float f1 = 0.0f
f1 ==> 0.0

jshell> Float f2 = -0.0f
f2 ==> -0.0

jshell> f1.equals(f2)
$3 ==> false

一方面,Groovy 嘗試緊密遵循 Java 行為,但另一方面,在更多地方自動在原始碼和包裝等效項之間切換。為避免混淆,我們建議遵循下列準則

  • 如果您希望區分正零和負零,請直接使用 equals 方法,或在使用 == 之前將任何原始碼轉換為其包裝等效項。

  • 如果您希望忽略正零和負零之間的差異,請直接使用 equalsIgnoreZeroSign 方法,或在使用 == 之前將任何非原始碼轉換為其原始等效項。

下列範例說明了這些準則

float f1 = 0.0f
float f2 = -0.0f
Float f3 = 0.0f
Float f4 = -0.0f

assert f1 == f2
assert (Float) f1 != (Float) f2

assert f3 != f4         (1)
assert (float) f3 == (float) f4

assert !f1.equals(f2)
assert !f3.equals(f4)

assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)
1 請記住,對於非原始碼,== 會對應到 .equals()

12. 轉換

Java 會自動進行擴展和縮小 轉換

表 1. Java 轉換

轉換為

轉換自

布林值

位元組

短整數

字元

整數

長整數

浮點數

雙精度浮點數

布林值

-

N

N

N

N

N

N

N

位元組

N

-

Y

C

Y

Y

Y

Y

短整數

N

C

-

C

Y

Y

Y

Y

字元

N

C

C

-

Y

Y

Y

Y

整數

N

C

C

C

-

Y

T

Y

長整數

N

C

C

C

C

-

T

T

浮點數

N

C

C

C

C

C

-

Y

雙精度浮點數

N

C

C

C

C

C

C

-

  • 'Y' 表示 Java 可以執行的轉換

  • 'C' 表示在有明確轉換時 Java 可以執行的轉換

  • 'T` 表示 Java 可以執行的轉換,但資料會被截斷

  • 'N' 表示 Java 無法執行的轉換

Groovy 大幅擴充了這一點。

表 2. Groovy 轉換

轉換為

轉換自

布林值

布林值

位元組

位元組

短整數

短整數

字元

字元

整數

整數

長整數

長整數

大整數

浮點數

浮點數

雙精度浮點數

雙精度浮點數

大十進位數

布林值

-

B

N

N

N

N

N

N

N

N

N

N

N

N

N

N

N

N

布林值

B

-

N

N

N

N

N

N

N

N

N

N

N

N

N

N

N

N

位元組

T

T

-

B

Y

Y

Y

D

Y

Y

Y

Y

Y

Y

Y

Y

Y

Y

位元組

T

T

B

-

Y

Y

Y

D

Y

Y

Y

Y

Y

Y

Y

Y

Y

Y

短整數

T

T

D

D

-

B

Y

D

Y

Y

Y

Y

Y

Y

Y

Y

Y

Y

短整數

T

T

D

T

B

-

Y

D

Y

Y

Y

Y

Y

Y

Y

Y

Y

Y

字元

T

T

Y

D

Y

D

-

D

Y

D

Y

D

D

Y

D

Y

D

D

字元

T

T

D

D

D

D

D

-

D

D

D

D

D

D

D

D

D

D

整數

T

T

D

D

D

D

Y

D

-

B

Y

Y

Y

Y

Y

Y

Y

Y

整數

T

T

D

D

D

D

Y

D

B

-

Y

Y

Y

Y

Y

Y

Y

Y

長整數

T

T

D

D

D

D

Y

D

D

D

-

B

Y

T

T

T

T

Y

長整數

T

T

D

D

D

T

Y

D

D

T

B

-

Y

T

T

T

T

Y

大整數

T

T

D

D

D

D

D

D

D

D

D

D

-

D

D

D

D

T

浮點數

T

T

D

D

D

D

T

D

D

D

D

D

D

-

B

Y

Y

Y

浮點數

T

T

D

T

D

T

T

D

D

T

D

T

D

B

-

Y

Y

Y

雙精度浮點數

T

T

D

D

D

D

T

D

D

D

D

D

D

D

D

-

B

Y

雙精度浮點數

T

T

D

T

D

T

T

D

D

T

D

T

D

D

T

B

-

Y

大十進位數

T

T

D

D

D

D

D

D

D

D

D

D

D

T

D

T

D

-

  • 'Y' 表示 Groovy 可以執行的轉換

  • 'D' 表示在動態編譯或明確轉換時 Groovy 可以執行的轉換

  • 'T' 表示 Groovy 可以執行的轉換,但資料會被截斷

  • 'B' 表示封裝/拆封操作

  • 'N' 表示 Groovy 無法執行的轉換。

在轉換為 布林值/布林值時,截斷會使用 Groovy Truth。從數字轉換為字元會將 Number.intvalue() 轉換為 char。Groovy 在從 浮點數雙精度浮點數 轉換時,會使用 Number.doubleValue() 建構 大整數大十進位數,否則會使用 toString() 建構。其他轉換的行為由 java.lang.Number 定義。

13. 額外關鍵字

Groovy 有許多與 Java 相同的關鍵字,而 Groovy 3 及以上版本也與 Java 有相同的保留類型 var。此外,Groovy 還有以下關鍵字

  • as

  • def

  • in

  • trait

  • it // 在閉包中

Groovy 比 Java 較不嚴格,它允許某些關鍵字出現在 Java 中會是非法的場合,例如以下內容是有效的:var var = [def: 1, as: 2, in: 3, trait: 4]。儘管如此,即使編譯器可能會滿意,也不建議在可能造成混淆的地方使用上述關鍵字。特別是,避免將它們用於變數、方法和類別名稱,因此我們先前的 var var 範例會被視為風格不佳。

有關 關鍵字 的其他文件。