鍍金池/ 教程/ Java/ 語(yǔ)法風(fēng)格指南
Grape 依賴管理器
與 Java 的區(qū)別
語(yǔ)法風(fēng)格指南
Groovy 開(kāi)發(fā)工具包
領(lǐng)域?qū)S谜Z(yǔ)言
安全更新
Groovy 與應(yīng)用的集成
運(yùn)行時(shí)及編譯時(shí)元編程(end)
測(cè)試指南
安裝 Groovy
設(shè)計(jì)模式
Groovy 的下載

語(yǔ)法風(fēng)格指南

愿意使用 Groovy 的 Java 開(kāi)發(fā)者往往還是會(huì)保留著 Java 的思維,通過(guò)對(duì) Groovy 的逐漸學(xué)習(xí),每次了解一個(gè)特性,他們的努力越來(lái)越具有成效,Groovy 代碼寫(xiě)得也越來(lái)越嫻熟。我們的文檔力圖繼續(xù)指導(dǎo)開(kāi)發(fā)者,教授一些常用的 Groovy 語(yǔ)法風(fēng)格、新的操作符,以及一些新的特性,比如閉包等。這篇指南并不完整,只能作為快速入門以及今后深入的奠基石,你可能以后會(huì)為本文檔貢獻(xiàn)內(nèi)容并對(duì)它作出一番改進(jìn)。

1. 不用分號(hào)

擁有 C/C++/C#/Java 背景的開(kāi)發(fā)者往往習(xí)慣于到處使用分號(hào)。更嚴(yán)重的是,Groovy 支持絕大部分的 Java 語(yǔ)法格式。因此,很容易就能將 Java 代碼復(fù)制粘貼到 Groovy 程序中繼續(xù)使用,其結(jié)果就是到處都是分號(hào)。但是,在 Groovy 中,分號(hào)是可選擇采用的,你可以忽略不用它們,而且往往這種方法才是地道的用法。

2. 可選擇性使用的 return 關(guān)鍵字

在 Groovy 中,方法主體內(nèi)部的最后一個(gè)求值表達(dá)式不必非得帶上 return 關(guān)鍵字就能返回。所以對(duì)于短方法和閉包而言,忽略這個(gè)關(guān)鍵字會(huì)顯得更簡(jiǎn)潔。

String toString() { return "a server" }
String toString() { "a server" }

但有時(shí)在使用變量時(shí),在兩行上分別出現(xiàn)了兩次這個(gè)變量,讓人看起來(lái)會(huì)不很舒服。

def props() {
    def m1 = [a: 1, b: 2]
    m2 = m1.findAll { k, v -> v % 2 == 0 }
    m2.c = 3
    m2
}

在這種情況下,在最后的表達(dá)式前換行,或者使用 return ,可讀性就會(huì)大大增強(qiáng)。

就我個(gè)人而言,并不一定會(huì)一直使用 return 關(guān)鍵字,這往往是憑感覺(jué)作出的。但在閉包中,我多數(shù)情況下不會(huì)使用它。所以如果該關(guān)鍵字是可選擇使用的,而如果它給你的感覺(jué)是為代碼可讀性套上了枷鎖,那么也你也可以不使用它,但這并非是強(qiáng)制性的。

然而,要提請(qǐng)大家注意的是,在使用 def 關(guān)鍵字(而非某種具體類型)定義的方法時(shí),最后一個(gè)表達(dá)式有時(shí)會(huì)被返回。所以建議最好指定某些具體的返回類型,比如 void 或某種其他類型。在上面所展示的例子中,假如我們把 m2 作為最后要返回的語(yǔ)句,那么最后的表達(dá)式應(yīng)該為 m2.c = 3,即返回 3,而并非是你所期望的映射。

if/else、try/catch 這些語(yǔ)句也能返回值,就好像在這些語(yǔ)句中也存在“最后一個(gè)表達(dá)式”一樣。

def foo(n) {
    if(n == 1) {
        "Roshan"
    } else {
        "Dawrani"
    }
}

assert foo(1) == "Roshan"
assert foo(2) == "Dawrani"

3. def 和類型

很多開(kāi)發(fā)者往往會(huì)同時(shí)使用 def 和類型,但這里的 def 是多余的。因此,要么使用 def,要么使用類型。

所以不要這樣寫(xiě):

def String name = "Guillaume"

這樣寫(xiě)就足夠了:

String name = "Guillaume"

在 Groovy 中使用 def 時(shí),實(shí)際的類型持有者是 Object,所以可以將任何對(duì)象賦予利用 def 定義的變量,如果一個(gè)方法聲明為返回 def 類型值,則它會(huì)返回任何類型的對(duì)象。

定義帶有無(wú)類型參數(shù)的方法時(shí),可以使用 def,但并不是必需條件,因此我們習(xí)慣上會(huì)忽略使用它。所以,與其采用如下方式:

void doSomething(def param1, def param2) { }

我們會(huì)更多建議采用如下方式:

void doSomething(param1, param2) { }

但正如我們?cè)谏弦还?jié)中所提到的那樣,為方法參數(shù)確定類型通常是一個(gè)不錯(cuò)的習(xí)慣,這樣做不僅能夠便于注釋代碼,而且也有助于 IDE 的代碼補(bǔ)全,或者利用 Groovy 的靜態(tài)類型檢查或靜態(tài)編譯功能。

另一個(gè) def 顯得多余并且應(yīng)該避免使用的地方是構(gòu)造函數(shù)的構(gòu)造:

class MyClass {
    def MyClass() {}
}

去掉 def 就可以了:

class MyClass {
    MyClass() {}
}

4. 默認(rèn)采用 public

默認(rèn)情況下,Groovy 會(huì)將類及方法認(rèn)為是 public 型,所以不必使用 public 修飾符了,只有當(dāng)非公開(kāi)時(shí),才需要加上。

所以與其這樣:

public class Server {
    public String toString() { return "a server" }
}

不如這樣:

class Server {
    String toString() { "a server" }
}

你可能還糾結(jié)于“包范圍內(nèi)”可見(jiàn)性這個(gè)問(wèn)題。事實(shí)上,Groovy 允許忽略 public 修飾符的潛臺(tái)詞即是說(shuō)默認(rèn)并不支持該范圍。但 Groovy 確實(shí)提供了一個(gè)注釋來(lái)實(shí)現(xiàn)這種可見(jiàn)性。

class Server {
    @PackageScope Cluster cluster
}

5. 省略括號(hào)

對(duì)于頂級(jí)表達(dá)式,Groovy 允許省去括號(hào),比如 println 命令:

println "Hello"
method a, b

對(duì)比一下之前的用法:

println("Hello")
method(a, b)

當(dāng)閉包成為方法調(diào)用的最后一個(gè)參數(shù)時(shí),比如在使用 Groovy 的 each{} 迭代機(jī)制時(shí),你可以將閉包放到括號(hào)對(duì)外面,甚至將括號(hào)對(duì)去除。

list.each( { println it } )
list.each(){ println it }
list.each  { println it }

一般往往推薦采用第三種方法,它顯得更自然一些。從語(yǔ)法層面上來(lái)看,內(nèi)容為空的括號(hào)對(duì)是一種無(wú)用的垃圾。

然而,在有些情況下,Groovy 是不允許去除括號(hào)的。遇到頂級(jí)的表達(dá)式,自然可以忽略括號(hào),但對(duì)于內(nèi)嵌的方法調(diào)用或在賦值語(yǔ)句的右側(cè),則是不允許忽略括號(hào)的。

def foo(n) { n }

println foo 1 // 不起作用   
def m = foo 1

6. 作為一等公民存在的類

Groovy 中并不需要 .class 后綴,這有點(diǎn)像 Java 中的 instanceof

比如:

connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)

使用之后介紹的 GString,應(yīng)用頭等公民的結(jié)果是這樣的:

connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)

7. Getter 與 Setter

Groovy 中的 getter 與 setter 構(gòu)成了我們稱之為 “屬性”(property)的形式,從而為訪問(wèn)這種屬性提供了一種快捷標(biāo)記。因此,我們完全可以舍棄 Java 式的調(diào)用方法,而采用字段樣式的訪問(wèn)標(biāo)記:

resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME

resourcePrototype.setName("something")
resourcePrototype.name = "something"

用 Groovy 編寫(xiě) bean 時(shí),通常會(huì)調(diào)用 POGO(普通 Groovy 對(duì)象),不必自己創(chuàng)建字段和 getter/setter,只需把這些活兒留給 Groovy 編譯器即可:

與其像下面這樣:

class Person {
    private String name
    String getName() { return name }
    void setName(String name) { this.name = name }
}

不如這樣寫(xiě),簡(jiǎn)單明快:

class Person {
    String name
}

如你所見(jiàn),實(shí)際上,沒(méi)有任何修飾符的獨(dú)立“字段”導(dǎo)致 Groovy 編譯器為你生成了一個(gè)私有字段和 getter 及 setter。

在使用這樣來(lái)自 Java 的POGO 時(shí),getter 與 setter 確實(shí)存在,當(dāng)然可以像通常那樣使用。

雖然編譯器創(chuàng)建了常見(jiàn)的 getter 和 setter 邏輯,但如果你希望在這些 getter/setter 中實(shí)現(xiàn)不同或者更多的邏輯,完全可以添加進(jìn)去,編譯器自會(huì)用你提供的邏輯來(lái)代替默認(rèn)生成的邏輯。

8. 利用命名參數(shù)及默認(rèn)構(gòu)造函數(shù)初始化 bean

假如有一個(gè)如下的 bean:

class Server {
    String name
    Cluster cluster
}

與其像下面這樣在隨后的語(yǔ)句中設(shè)置每一個(gè) setter:

def server = new Server()
server.name = "Obelix"
server.cluster = aCluster

可以利用命名參數(shù)及默認(rèn)構(gòu)造函數(shù)(首先調(diào)用該構(gòu)造函數(shù),然后 setter 按照它們?cè)谟成渲兴付ǖ捻樞虮灰来握{(diào)用)來(lái)設(shè)置:

def server = new Server(name: "Obelix", cluster: aCluster)   

9. 利用 with() 來(lái)處理對(duì)于同一 bean 的重復(fù)操作

在創(chuàng)建新實(shí)例時(shí),帶有默認(rèn)構(gòu)造函數(shù)的命名參數(shù)是非常有用的。但是,如果更新一個(gè)已有實(shí)例呢?難道你還必須一遍一遍重復(fù) server 前綴?不必如此,Groovy 所提供的 with() 方法可以應(yīng)用于所有類型的對(duì)象,比如像下面這樣:

server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()

就可以轉(zhuǎn)換成如下的形式:

server.with {
    name = application.name
    status = status
    sessionCount = 3
    start()
    stop()
}

10. 相等與 ==

Java 的 == 實(shí)際相當(dāng)于 Groovy 的 is() 方法,而 Groovy 的 == 則是一個(gè)更巧妙的 equals()。

要想比較對(duì)象的引用,不能用 ==,而應(yīng)該用 a.is(b)。

但要想進(jìn)行常見(jiàn)的 equals() 比對(duì),應(yīng)該首選使用 Groovy 的 ==,因?yàn)樗沧⒁獗苊?NullPointerException,而與等號(hào)左右兩邊是否為 null 無(wú)關(guān)。

所以與其這樣:

status != null && status.equals(ControlConstants.STATUS_COMPLETED)

不如這樣:

status == ControlConstants.STATUS_COMPLETED    

11. GString(插值、多行)

在 Java 中,我們常常聯(lián)合使用字符串與變量,通常會(huì)帶有很多開(kāi)閉的雙引號(hào)、加號(hào),以及用于換行的 \n 字符。利用插入字符串(也叫 GString),以前的字符串看起來(lái)就會(huì)優(yōu)雅多了,輸入起來(lái)也變得簡(jiǎn)潔了:

throw new Exception("Unable to convert resource: " + resource)

跟下面的方式對(duì)比一下:

throw new Exception("Unable to convert resource: ${resource}")

在大括號(hào)內(nèi),可以放入各種表達(dá)式,而不只是變量。對(duì)于較簡(jiǎn)單的變量,或者 variable.property,甚至還可以去掉大括號(hào)。

throw new Exception("Unable to convert resource: $resource")

甚至還可以使用 ${→ resource } 和閉包形式來(lái)拖延計(jì)算那些表達(dá)式。當(dāng) GString 被迫轉(zhuǎn)換為字符串時(shí),就會(huì)計(jì)算閉包,獲得返回值的 toString() 表示形式。

范例:

int i = 3

def s1 = "i's value is: ${i}"
def s2 = "i's value is: ${-> i}"

i++

assert s1 == "i's value is: 3" // 急切地計(jì)算,一創(chuàng)建時(shí)就求值
assert s2 == "i's value is: 4" // 拖延式計(jì)算,考慮新值   

當(dāng)字符串與它們的聯(lián)合表達(dá)式用 Java 表示顯得很長(zhǎng)時(shí),比如像下面這個(gè):

throw new PluginException("Failed to execute command list-applications:" +
    " The group with name " +
    parameterMap.groupname[0] +
    " is not compatible group of type " +
    SERVER_TYPE_NAME)

你可以使用 \ 行連續(xù)字符(這并不是一個(gè)多行字符串):

throw new PluginException("Failed to execute command list-applications: \
The group with name ${parameterMap.groupname[0]} \
is not compatible group of type ${SERVER_TYPE_NAME}")

或者利用三個(gè)引號(hào)的多行字符串來(lái)表示:

throw new PluginException("""Failed to execute command list-applications:
    The group with name ${parameterMap.groupname[0]}
    is not compatible group of type ${SERVER_TYPE_NAME)}""")

另外,還可以在多行字符串調(diào)用 .stripIndent() 去除字符串左邊的縮進(jìn)。

注意,在 Groovy 中,單引號(hào)與雙引號(hào)的區(qū)別在于:?jiǎn)我?hào)常用于創(chuàng)建沒(méi)有插入變量的 Java 字符串,而雙引號(hào)則既能創(chuàng)建 Java 字符串,也能在出現(xiàn)插值變量時(shí)創(chuàng)建 GString。

對(duì)于多行字符串,可以使用三重引號(hào),比如對(duì) GString 用三重雙引號(hào),對(duì)單純的字符串用三重單引號(hào)。

如果需要編寫(xiě)正則表達(dá)式模式,應(yīng)該使用“斜杠式”字符串標(biāo)記法:

assert "foooo/baaaaar" ==~ /fo+\/ba+r/  

這樣寫(xiě)的好處在于不必使用雙重轉(zhuǎn)義反斜杠,從而更便于使用 regex。

最后要強(qiáng)調(diào)的是,在需要字符串常量時(shí),盡量?jī)?yōu)先使用單引號(hào)字符串,而在顯然需要字符串插值時(shí),才使用雙引號(hào)字符串。

12. 數(shù)據(jù)結(jié)構(gòu)的原生語(yǔ)法

Groovy 為一些數(shù)據(jù)結(jié)構(gòu)(如列表、映射、正則表達(dá)式以及值范圍)提供了原生的語(yǔ)法結(jié)構(gòu),一定要利用好它們。

下面是一些原生構(gòu)造:

def list = [1, 4, 6, 9]

// 默認(rèn),鍵是 String 類型,所以不需要用引號(hào)括起來(lái)
// 你可以用像 [(variableStateAcronym): stateName] 這樣的帶有 () 的結(jié)構(gòu)來(lái)封裝鍵,插入變量或?qū)ο? 

def map = [CA: 'California', MI: 'Michigan']

def range = 10..20
def pattern = ~/fo*/

// 等同于 add()  
list << 5

// 調(diào)用 contains()
assert 4 in list
assert 5 in list
assert 15 in range

// 下標(biāo)符號(hào)  
assert list[1] == 4

// 添加一個(gè)新的鍵值對(duì)   
map << [WA: 'Washington']
// 下標(biāo)符號(hào)  
assert map['CA'] == 'California'
// 屬性標(biāo)記  
assert map.WA == 'Washington'

// 判斷字符串是否與模式匹配   
assert 'foo' =~ pattern

13. Groovy 開(kāi)發(fā)工具包

繼續(xù)探討數(shù)據(jù)結(jié)構(gòu),在需要對(duì)集合迭代時(shí),Groovy 提供了多種方法,通過(guò)裝飾模式強(qiáng)化 Java 的核心數(shù)據(jù)結(jié)構(gòu),比如:each{}、find{}、findAll{}、every{}、collect{}以及inject{}等。這些方法不僅為編程語(yǔ)言提供了功能性幫助,而且還能便于人們實(shí)現(xiàn)復(fù)雜的算法。通過(guò)裝飾模式,很多新方法已經(jīng)添加到不同的類型中,這要感謝語(yǔ)言本身的動(dòng)態(tài)特性??梢栽谙旅孢@個(gè)網(wǎng)站找到很多的有用方法,它們可以用于字符串、文件、流以及集合等:http://beta.groovy-lang.org/gdk.html

14. switch 的魔力

switch 在 Groovy 中的作用要比在 C 族語(yǔ)言中更為強(qiáng)大,后者往往只接受原語(yǔ)并將其同化。Groovy 中的 switch 能夠接受更多的類型。

def x = 1.23
def result = ""
switch (x) {
    case "foo": result = "found foo"
    // lets fall through
    case "bar": result += "bar"
    case [4, 5, 6, 'inList']:
        result = "list"
        break
    case 12..30:
        result = "range"
        break
    case Integer:
        result = "integer"
        break
    case Number:
        result = "number"
        break
    case { it > 3 }:
        result = "number > 3"
        break
    default: result = "default"
}
assert result == "number"

一般地說(shuō),利用 isCase() 方法可以確定值是否對(duì)應(yīng)一個(gè) case。

15. 導(dǎo)入別名

在 Java 中,使用不同包而同名的兩個(gè)類時(shí)(比如 java.util.Listjava.awt.List 這兩個(gè)包),你可以導(dǎo)入其中一個(gè)類,而對(duì)另一個(gè)類使用完整限定名。

有時(shí)在代碼中經(jīng)常使用長(zhǎng)類名,代碼就會(huì)變得冗長(zhǎng)啰嗦。

為了改善這種狀況,Groovy 提供了導(dǎo)入別名機(jī)制。

import java.util.List as juList
import java.awt.List as aList

import java.awt.WindowConstants as WC

還可以靜態(tài)地導(dǎo)入方法:

import static pkg.SomeClass.foo
foo()

16. Groovy Truth

任何對(duì)象都可以被強(qiáng)制轉(zhuǎn)換為布爾值:任何為 null、void的對(duì)象,等同于 0 或空的值,都會(huì)解析為 false,凡之則為 true

所以不必這樣寫(xiě):

if (name != null && name.length > 0) {}

只需這樣寫(xiě)就好了:

if (name) {}

這一原則也可以用于集合等對(duì)象。

因此,可以在諸如 while()、if()、三元運(yùn)算子以及 Elvis 操作符等結(jié)構(gòu)中使用一些快捷形式。

甚至可以自定義 Groovy Truth 對(duì)象,只需為類加入一個(gè) asBoolean() 布爾方法即可。

17.

為了安全地在對(duì)象圖表中導(dǎo)航,Groovy 支持 . 操作符的一個(gè)變體。

在 Java 中,如果你對(duì)圖表中的某個(gè)較深的節(jié)點(diǎn)比較感興趣,需要檢查 null,你可能經(jīng)常會(huì)寫(xiě)復(fù)雜的 if 或內(nèi)嵌的 if 語(yǔ)句,就像下面這樣:

if (order != null) {
    if (order.getCustomer() != null) {
        if (order.getCustomer().getAddress() != null) {
            System.out.println(order.getCustomer().getAddress());
        }
    }
}

利用 ?. 安全解除引用操作符,可以將上面的代碼利用下面的形式來(lái)簡(jiǎn)化:

println order?.customer?.address

會(huì)在調(diào)用鏈中檢查 null 值,如果有元素為 null,則不會(huì)拋出 NullPointerException 異常。如果有 元素為 null,則結(jié)果值必為 null。

18. 斷言

可以使用 assert 語(yǔ)句來(lái)檢查參數(shù)、返回值以及更多類型的值。

與 Java 的 assert 有所不同,Groovy 的 assert 并不需要激活,它是一直被檢查的。

def check(String name) {
    // 根據(jù) Groovy Truth,name 應(yīng)為非 null 與非空
    assert name
    // 安全導(dǎo)航 + Groovy Truth  
    assert name?.size() > 3
}

另外要注意的是,Groovy 的 “強(qiáng)力斷言” 語(yǔ)句提供的輸出結(jié)果是很出色的,在生成的圖表中對(duì)每個(gè)子表達(dá)式的各種值都進(jìn)行了斷言。

19. 用于默認(rèn)值的 Elvis 操作符

Elvis 操作符是一種特殊的三元操作符,對(duì)于處理默認(rèn)值來(lái)說(shuō)不啻是一種快捷方式。

我們往往會(huì)像下面這樣來(lái)書(shū)寫(xiě):

def result = name != null ? name : "Unknown"

多虧有了 Groovy Truth,null 檢查可以簡(jiǎn)化為只用 name 就可以了。

進(jìn)一步來(lái)說(shuō),既然要返回 name,那么與其在這個(gè)三元表達(dá)式中重復(fù)兩次名稱,不如去掉問(wèn)號(hào)和冒號(hào)之間的東西,使用 Elvis 操作符,可以這樣來(lái)完成:

def result = name ?: "Unknown"

20. 異常捕捉

如果不關(guān)心 try 語(yǔ)句塊中所要拋出的異常類型,可以只捕捉異常而忽略它們的類型。所以,像下面這樣的語(yǔ)句:

try {
    // ...
} catch (Exception t) {
    // 一些糟糕的事情   
}

就可以變成下面這樣捕捉任何異常(anyall 都可以,只要是能讓你認(rèn)為是任何東西的詞兒就可以用):

try {
    // ...
} catch (any) {
    // 一些糟糕的事情  
}

它會(huì)捕捉所有異常,而并不僅是 Throwable 的異常。如果需要捕捉的是“每一個(gè)”異常,必須明確地聲明要捕捉的是 Throwable 異常。

21. 額外的類型建議

最后講講什么時(shí)候以及如何使用可選類型。Groovy 允許自己決定是否使用顯式的強(qiáng)類型,或何時(shí)使用 def。

簡(jiǎn)單的經(jīng)驗(yàn)法則是:如果你寫(xiě)的代碼將被其他人用作公共 API,你就應(yīng)該使用強(qiáng)類型,它能有助于合約的健壯性,避免可能通過(guò)的參數(shù)類型錯(cuò)誤,形成更好的文檔,有助于 IDE 自動(dòng)完成代碼。假如代碼只是自用,比如私有方法,或 IDE 能夠輕松地推斷類型,那么你就可以更自由地確定何時(shí)利用類型。