Groovy 試圖盡可能地讓 Java 開發(fā)者快速適應。在設計 Groovy 時,我們努力不讓用戶感到驚訝,即遵循“最小驚訝”原則,特別是針對那些此前有 Java 開發(fā)背景的 Groovy 初學者。
下面講講 Groovy 與 Java 的主要不同點。
下面這些包和類都是默認導入的,也就是說,不用再顯式地使用 import
語句了:
在 Groovy 中,調用的方法將在運行時被選擇。這種機制被稱為運行時分派或多重方法(multi-methods),是根據運行時實參(argument)的類型來選擇方法。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 使用靜態(tài)數(shù)據類型,o
被聲明為 Object
對象,而 Groovy 會在運行時實際調用方法時進行選擇。因為調用的是 String
類型的對象,所以自然調用 String
版本的方法。
在 Groovy 中,{...}
語句塊是留給閉包(Closure)使用的,這意味著你不能使用以下這種格式來創(chuàng)建數(shù)組:
int[] array = { 1, 2, 3}
正確的方式是這樣的:
int[] array = [1,2,3]
在 Groovy 中,如果某個字段缺失了修飾符,并不會導致像在 Java 中那樣形成包的私有字段:
class Person {
String name
}
相反,它會用來創(chuàng)建一個屬性(property),也就是一個私有字段(private field),以及一個關聯(lián)的 getter 和一個關聯(lián)的 setter。
在 Groovy 中創(chuàng)建包私有字段,可以通過標注 @PackageScope
來實現(xiàn)。
class Person {
@PackageScope String name
}
ARM(Automatic Resource Management,自動資源管理)語句塊從 Java 7 開始引入,但 Groovy 并不支持。相反,Groovy 提供多種基于閉包的方法,效果相同但卻合乎習慣。例如:
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();
}
可以寫成下面這樣的代碼:
new File('/path/to/file').eachLine('UTF-8') {
println it
}
或者,如果你想讓它更接近于 Java 的慣用形式,可以這樣寫:
new File('/path/to/file').withReader('UTF-8') { reader ->
reader.eachLine {
println it
}
}
Groovy 中的匿名內部類和內嵌類的實現(xiàn)跟 Java 是一樣的,但你不應拿 Java 語言規(guī)范來考量它,應對差異情況保持冷靜與寬容。已完成的實現(xiàn)看起來有點類似 groovy.lang.Closure
類的實現(xiàn)。一方面,這使得訪問私有字段和方法可能會比較麻煩,但另一方面,本地變量也不必非要設置成 final
了。
下面是一個靜態(tài)內部類的例子:
class A {
static class B {}
}
new A.B()
靜態(tài)內部類是最受支持的,所以如果你確實需要一個內部類,應該首先考慮靜態(tài)內部類。
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)
在 Java 中,你可以這樣做:
public class Y {
public class X {}
public X foo() {
return new X();
}
public static X createX(Y y) {
return y.new X();
}
}
而 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。調用構造函數(shù)通常也適用于此規(guī)則。不過危險的是,以上述代碼為例,你必須寫為 new X()
,而不是new X(this)
。這可能也是一種處理該問題的常規(guī)方法,目前對此也沒有更好的解決辦法。
Java 8 支持 Lambda 表達式和方法引用:
Runnable run = () -> System.out.println("Run");
list.forEach(System.out::println);
Java 8 的 lambda 幾乎可以認為是匿名內部函數(shù)。Groovy 不支持這種格式,而采用閉包來實現(xiàn)。
Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)
由于雙引號所包括起來的字符串字面量會被解釋為 GString
值(即 “Groovy 字符串”的簡稱),所以如果當某個類中的 String
字面量含有美元字符($
)時,那么利用 Groovy 和 Java 編譯器進行編譯時,Groovy 很可能就會編譯失敗,或者產生與 Java 編譯所不同的結果。
通常,如果某個 API 聲明了形參的類型,Groovy 會自動轉換 GString
和 String
。要小心那些形參為 Object
的 Java API,需要檢查它們的實際類型。
在 Groovy 中,由單引號所創(chuàng)建的字面量屬于 String
類型對象,而雙引號創(chuàng)建的字面量則可能是 String
或 GString
對象,具體分類由字面量中是否有插值來決定。
assert 'c'.getClass()==String
assert "c".getClass()==String
assert "c${1}".getClass() in GString
當把一個包含單個字符的 String
對象賦予一個 char
類型變量時,Groovy 會自動將該對象轉換為 char
類型。在調用帶有 char
類型實參的方法時,我們需要顯式地轉換參數(shù)值,或者確認參數(shù)值已經預先轉換過了。
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 模式的轉換則出現(xiàn)異常錯誤,從而失敗。
// 對于僅包含單個字符的字符串,兩種轉換模式是等效的
assert ((char) "c").class==Character
assert ("c" as char).class==Character
// 對于包含多個字符的字符串來說,兩種模式的結果并不相同
try {
((char) 'cx') == 'c'
assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'
==
的行為差異在 Java 中,==
代表基本數(shù)據類型的相同,或對象引用的等價性。在 Groovy 中,==
的含義變成了 a.compareTo(b)==0
,不過這要當且僅當 ==
兩邊的對象都實現(xiàn)了 Comparable
接口時才能實現(xiàn),否則 ==
就等同于 a.equals(b)
。而要想在 Groovy 中檢查對象間的引用等價性,則需使用 is
,比如:a.is(b)
。
Java 能夠自動執(zhí)行一些擴寬轉換和縮減轉換,關于轉換的概念參見這里。
轉換目的類型 | ||||||||
---|---|---|---|---|---|---|---|---|
轉換源類型 | boolean | byte | short | char | int | long | float | double |
boolean | - | N | N | N | N | N | N | N |
byte | N | - | Y | C | Y | Y | Y | Y |
short | N | C | - | C | Y | Y | Y | Y |
char | N | C | C | - | Y | Y | Y | Y |
int | N | C | C | C | - | Y | T | Y |
long | N | C | C | C | C | - | T | T |
float | N | C | C | C | C | C | - | Y |
double | N | C | C | C | C | C | C | - |
* Y
表示 Java 可以執(zhí)行的轉換。C
表示的是在顯式轉換時 Java 能夠執(zhí)行的轉換。T
表示 Java 可以執(zhí)行的轉換,但數(shù)據被截斷了。N
表示 Java 所不能實行的轉換。
Groovy 大大擴展這些轉換。
轉換目的類型 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
轉換源類型 | boolean | Boolean | byte | Byte | short | Short | char | Character | int | Integer | long | Long | BigInteger | float | Float | double | Double | BigDecimal |
boolean | - | B | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N |
Boolean | B | - | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N |
byte | T | T | - | B | Y | Y | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Byte | T | T | B | - | Y | Y | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
short | T | T | D | D | - | B | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Short | T | T | D | T | B | - | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
char | T | T | Y | D | Y | D | - | D | Y | D | Y | D | D | Y | D | Y | D | D |
Character | T | T | D | D | D | D | D | - | D | D | D | D | D | D | D | D | D | D |
int | T | T | D | D | D | D | Y | D | - | B | Y | Y | Y | Y | Y | Y | Y | Y |
Integer | T | T | D | D | D | D | Y | D | B | - | Y | Y | Y | Y | Y | Y | Y | Y |
long | T | T | D | D | D | D | Y | D | D | D | - | B | Y | T | T | T | T | Y |
Long | T | T | D | D | D | T | Y | D | D | T | B | - | Y | T | T | T | T | Y |
BigInteger | T | T | D | D | D | D | D | D | D | D | D | D | - | D | D | D | D | T |
float | T | T | D | D | D | D | T | D | D | D | D | D | D | - | B | Y | Y | Y |
Float | T | T | D | T | D | T | T | D | D | T | D | T | D | B | - | Y | Y | Y |
double | T | T | D | D | D | D | T | D | D | D | D | D | D | D | D | - | B | Y |
Double | T | T | D | T | D | T | T | D | D | T | D | T | D | D | T | B | - | Y |
BigDecimal | T | T | D | D | D | D | D | D | D | D | D | D | D | T | D | T | D | - |
* Y
表示 Groovy 可以執(zhí)行的轉換。D
表示的是在動態(tài)編譯或顯式轉換時 Groovy 能夠執(zhí)行的轉換。T
表示 Groovy 可以執(zhí)行的轉換,但數(shù)據被截斷了。B
表示裝箱/拆箱操作。N
表示 Groovy 不能實行的轉換。
在轉換為 boolean
/Boolean
時,截斷使用的是 Groovy Truth。從數(shù)值轉換為字符是將 Number.intvalue()
轉換為 char
。從 Float
或 Double
轉換時,Groovy 使用 Number.doubleValue()
來構建 BigInteger
和 BigDecimal
,否則將使用 toString()
來構建。其他轉換則有 java.lang.Number
所定義的行為。
Groovy 的關鍵字要比 Java 中的多一些。不要使用以下這些關鍵字來定義變量名等名稱:
as
def
in
trait
default
必須位于 swith / case 結構的結尾default
必須位于 swith / case 結構的結尾。在 Java 中,它可以放在 swith / case 結構中的任何位置,但在 Groovy 中,它更像是一個 else 子句,而非一個默認的 case 子句。