與 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 會在 GString
和 String
之間自動轉換,但請注意接受 Object
參數並檢查實際類型的 Java API。
9. 字串和字元文字
Groovy 中的單引號文字用於 String
,而雙引號則會產生 String
或 GString
,具體取決於文字中是否有內插。
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 會自動進行擴展和縮小 轉換。
轉換為 |
||||||||
轉換自 |
布林值 |
位元組 |
短整數 |
字元 |
整數 |
長整數 |
浮點數 |
雙精度浮點數 |
布林值 |
- |
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 大幅擴充了這一點。
轉換為 |
||||||||||||||||||
轉換自 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- |
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
範例會被視為風格不佳。
有關 關鍵字 的其他文件。