愿意使用 Groovy 的 Java 開發(fā)者往往還是會保留著 Java 的思維,通過對 Groovy 的逐漸學習,每次了解一個特性,他們的努力越來越具有成效,Groovy 代碼寫得也越來越嫻熟。我們的文檔力圖繼續(xù)指導開發(fā)者,教授一些常用的 Groovy 語法風格、新的操作符,以及一些新的特性,比如閉包等。這篇指南并不完整,只能作為快速入門以及今后深入的奠基石,你可能以后會為本文檔貢獻內容并對它作出一番改進。
擁有 C/C++/C#/Java 背景的開發(fā)者往往習慣于到處使用分號。更嚴重的是,Groovy 支持絕大部分的 Java 語法格式。因此,很容易就能將 Java 代碼復制粘貼到 Groovy 程序中繼續(xù)使用,其結果就是到處都是分號。但是,在 Groovy 中,分號是可選擇采用的,你可以忽略不用它們,而且往往這種方法才是地道的用法。
return
關鍵字在 Groovy 中,方法主體內部的最后一個求值表達式不必非得帶上 return
關鍵字就能返回。所以對于短方法和閉包而言,忽略這個關鍵字會顯得更簡潔。
String toString() { return "a server" }
String toString() { "a server" }
但有時在使用變量時,在兩行上分別出現(xiàn)了兩次這個變量,讓人看起來會不很舒服。
def props() {
def m1 = [a: 1, b: 2]
m2 = m1.findAll { k, v -> v % 2 == 0 }
m2.c = 3
m2
}
在這種情況下,在最后的表達式前換行,或者使用 return
,可讀性就會大大增強。
就我個人而言,并不一定會一直使用 return
關鍵字,這往往是憑感覺作出的。但在閉包中,我多數(shù)情況下不會使用它。所以如果該關鍵字是可選擇使用的,而如果它給你的感覺是為代碼可讀性套上了枷鎖,那么也你也可以不使用它,但這并非是強制性的。
然而,要提請大家注意的是,在使用 def
關鍵字(而非某種具體類型)定義的方法時,最后一個表達式有時會被返回。所以建議最好指定某些具體的返回類型,比如 void 或某種其他類型。在上面所展示的例子中,假如我們把 m2 作為最后要返回的語句,那么最后的表達式應該為 m2.c = 3
,即返回 3
,而并非是你所期望的映射。
像 if
/else
、try
/catch
這些語句也能返回值,就好像在這些語句中也存在“最后一個表達式”一樣。
def foo(n) {
if(n == 1) {
"Roshan"
} else {
"Dawrani"
}
}
assert foo(1) == "Roshan"
assert foo(2) == "Dawrani"
def
和類型很多開發(fā)者往往會同時使用 def
和類型,但這里的 def
是多余的。因此,要么使用 def
,要么使用類型。
所以不要這樣寫:
def String name = "Guillaume"
這樣寫就足夠了:
String name = "Guillaume"
在 Groovy 中使用 def
時,實際的類型持有者是 Object
,所以可以將任何對象賦予利用 def
定義的變量,如果一個方法聲明為返回 def
類型值,則它會返回任何類型的對象。
定義帶有無類型參數(shù)的方法時,可以使用 def
,但并不是必需條件,因此我們習慣上會忽略使用它。所以,與其采用如下方式:
void doSomething(def param1, def param2) { }
我們會更多建議采用如下方式:
void doSomething(param1, param2) { }
但正如我們在上一節(jié)中所提到的那樣,為方法參數(shù)確定類型通常是一個不錯的習慣,這樣做不僅能夠便于注釋代碼,而且也有助于 IDE 的代碼補全,或者利用 Groovy 的靜態(tài)類型檢查或靜態(tài)編譯功能。
另一個 def
顯得多余并且應該避免使用的地方是構造函數(shù)的構造:
class MyClass {
def MyClass() {}
}
去掉 def
就可以了:
class MyClass {
MyClass() {}
}
public
默認情況下,Groovy 會將類及方法認為是 public
型,所以不必使用 public
修飾符了,只有當非公開時,才需要加上。
所以與其這樣:
public class Server {
public String toString() { return "a server" }
}
不如這樣:
class Server {
String toString() { "a server" }
}
你可能還糾結于“包范圍內”可見性這個問題。事實上,Groovy 允許忽略 public
修飾符的潛臺詞即是說默認并不支持該范圍。但 Groovy 確實提供了一個注釋來實現(xiàn)這種可見性。
class Server {
@PackageScope Cluster cluster
}
對于頂級表達式,Groovy 允許省去括號,比如 println
命令:
println "Hello"
method a, b
對比一下之前的用法:
println("Hello")
method(a, b)
當閉包成為方法調用的最后一個參數(shù)時,比如在使用 Groovy 的 each{}
迭代機制時,你可以將閉包放到括號對外面,甚至將括號對去除。
list.each( { println it } )
list.each(){ println it }
list.each { println it }
一般往往推薦采用第三種方法,它顯得更自然一些。從語法層面上來看,內容為空的括號對是一種無用的垃圾。
然而,在有些情況下,Groovy 是不允許去除括號的。遇到頂級的表達式,自然可以忽略括號,但對于內嵌的方法調用或在賦值語句的右側,則是不允許忽略括號的。
def foo(n) { n }
println foo 1 // 不起作用
def m = foo 1
Groovy 中并不需要 .class
后綴,這有點像 Java 中的 instanceof
。
比如:
connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)
使用之后介紹的 GString,應用頭等公民的結果是這樣的:
connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)
Groovy 中的 getter 與 setter 構成了我們稱之為 “屬性”(property)的形式,從而為訪問這種屬性提供了一種快捷標記。因此,我們完全可以舍棄 Java 式的調用方法,而采用字段樣式的訪問標記:
resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME
resourcePrototype.setName("something")
resourcePrototype.name = "something"
用 Groovy 編寫 bean 時,通常會調用 POGO(普通 Groovy 對象),不必自己創(chuàng)建字段和 getter/setter,只需把這些活兒留給 Groovy 編譯器即可:
與其像下面這樣:
class Person {
private String name
String getName() { return name }
void setName(String name) { this.name = name }
}
不如這樣寫,簡單明快:
class Person {
String name
}
如你所見,實際上,沒有任何修飾符的獨立“字段”導致 Groovy 編譯器為你生成了一個私有字段和 getter 及 setter。
在使用這樣來自 Java 的POGO 時,getter 與 setter 確實存在,當然可以像通常那樣使用。
雖然編譯器創(chuàng)建了常見的 getter 和 setter 邏輯,但如果你希望在這些 getter/setter 中實現(xiàn)不同或者更多的邏輯,完全可以添加進去,編譯器自會用你提供的邏輯來代替默認生成的邏輯。
假如有一個如下的 bean:
class Server {
String name
Cluster cluster
}
與其像下面這樣在隨后的語句中設置每一個 setter:
def server = new Server()
server.name = "Obelix"
server.cluster = aCluster
可以利用命名參數(shù)及默認構造函數(shù)(首先調用該構造函數(shù),然后 setter 按照它們在映射中所指定的順序被依次調用)來設置:
def server = new Server(name: "Obelix", cluster: aCluster)
with()
來處理對于同一 bean 的重復操作在創(chuàng)建新實例時,帶有默認構造函數(shù)的命名參數(shù)是非常有用的。但是,如果更新一個已有實例呢?難道你還必須一遍一遍重復 server
前綴?不必如此,Groovy 所提供的 with()
方法可以應用于所有類型的對象,比如像下面這樣:
server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()
就可以轉換成如下的形式:
server.with {
name = application.name
status = status
sessionCount = 3
start()
stop()
}
==
Java 的 ==
實際相當于 Groovy 的 is()
方法,而 Groovy 的 ==
則是一個更巧妙的 equals()
。
要想比較對象的引用,不能用 ==
,而應該用 a.is(b)
。
但要想進行常見的 equals()
比對,應該首選使用 Groovy 的 ==
,因為它也注意避免 NullPointerException
,而與等號左右兩邊是否為 null
無關。
所以與其這樣:
status != null && status.equals(ControlConstants.STATUS_COMPLETED)
不如這樣:
status == ControlConstants.STATUS_COMPLETED
在 Java 中,我們常常聯(lián)合使用字符串與變量,通常會帶有很多開閉的雙引號、加號,以及用于換行的 \n
字符。利用插入字符串(也叫 GString),以前的字符串看起來就會優(yōu)雅多了,輸入起來也變得簡潔了:
throw new Exception("Unable to convert resource: " + resource)
跟下面的方式對比一下:
throw new Exception("Unable to convert resource: ${resource}")
在大括號內,可以放入各種表達式,而不只是變量。對于較簡單的變量,或者 variable.property
,甚至還可以去掉大括號。
throw new Exception("Unable to convert resource: $resource")
甚至還可以使用 ${→ resource }
和閉包形式來拖延計算那些表達式。當 GString 被迫轉換為字符串時,就會計算閉包,獲得返回值的 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" // 急切地計算,一創(chuàng)建時就求值
assert s2 == "i's value is: 4" // 拖延式計算,考慮新值
當字符串與它們的聯(lián)合表達式用 Java 表示顯得很長時,比如像下面這個:
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ù)字符(這并不是一個多行字符串):
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}")
或者利用三個引號的多行字符串來表示:
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)}""")
另外,還可以在多行字符串調用 .stripIndent()
去除字符串左邊的縮進。
注意,在 Groovy 中,單引號與雙引號的區(qū)別在于:單引號常用于創(chuàng)建沒有插入變量的 Java 字符串,而雙引號則既能創(chuàng)建 Java 字符串,也能在出現(xiàn)插值變量時創(chuàng)建 GString。
對于多行字符串,可以使用三重引號,比如對 GString 用三重雙引號,對單純的字符串用三重單引號。
如果需要編寫正則表達式模式,應該使用“斜杠式”字符串標記法:
assert "foooo/baaaaar" ==~ /fo+\/ba+r/
這樣寫的好處在于不必使用雙重轉義反斜杠,從而更便于使用 regex。
最后要強調的是,在需要字符串常量時,盡量優(yōu)先使用單引號字符串,而在顯然需要字符串插值時,才使用雙引號字符串。
Groovy 為一些數(shù)據(jù)結構(如列表、映射、正則表達式以及值范圍)提供了原生的語法結構,一定要利用好它們。
下面是一些原生構造:
def list = [1, 4, 6, 9]
// 默認,鍵是 String 類型,所以不需要用引號括起來
// 你可以用像 [(variableStateAcronym): stateName] 這樣的帶有 () 的結構來封裝鍵,插入變量或對象
def map = [CA: 'California', MI: 'Michigan']
def range = 10..20
def pattern = ~/fo*/
// 等同于 add()
list << 5
// 調用 contains()
assert 4 in list
assert 5 in list
assert 15 in range
// 下標符號
assert list[1] == 4
// 添加一個新的鍵值對
map << [WA: 'Washington']
// 下標符號
assert map['CA'] == 'California'
// 屬性標記
assert map.WA == 'Washington'
// 判斷字符串是否與模式匹配
assert 'foo' =~ pattern
繼續(xù)探討數(shù)據(jù)結構,在需要對集合迭代時,Groovy 提供了多種方法,通過裝飾模式強化 Java 的核心數(shù)據(jù)結構,比如:each{}
、find{}
、findAll{}
、every{}
、collect{}
以及inject{}
等。這些方法不僅為編程語言提供了功能性幫助,而且還能便于人們實現(xiàn)復雜的算法。通過裝飾模式,很多新方法已經(jīng)添加到不同的類型中,這要感謝語言本身的動態(tài)特性??梢栽谙旅孢@個網(wǎng)站找到很多的有用方法,它們可以用于字符串、文件、流以及集合等:http://beta.groovy-lang.org/gdk.html。
switch
的魔力switch
在 Groovy 中的作用要比在 C 族語言中更為強大,后者往往只接受原語并將其同化。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"
一般地說,利用 isCase()
方法可以確定值是否對應一個 case。
在 Java 中,使用不同包而同名的兩個類時(比如 java.util.List
和 java.awt.List
這兩個包),你可以導入其中一個類,而對另一個類使用完整限定名。
有時在代碼中經(jīng)常使用長類名,代碼就會變得冗長啰嗦。
為了改善這種狀況,Groovy 提供了導入別名機制。
import java.util.List as juList
import java.awt.List as aList
import java.awt.WindowConstants as WC
還可以靜態(tài)地導入方法:
import static pkg.SomeClass.foo
foo()
任何對象都可以被強制轉換為布爾值:任何為 null
、void
的對象,等同于 0 或空的值,都會解析為 false
,凡之則為 true
。
所以不必這樣寫:
if (name != null && name.length > 0) {}
只需這樣寫就好了:
if (name) {}
這一原則也可以用于集合等對象。
因此,可以在諸如 while()
、if()
、三元運算子以及 Elvis 操作符等結構中使用一些快捷形式。
甚至可以自定義 Groovy Truth 對象,只需為類加入一個 asBoolean()
布爾方法即可。
為了安全地在對象圖表中導航,Groovy 支持 .
操作符的一個變體。
在 Java 中,如果你對圖表中的某個較深的節(jié)點比較感興趣,需要檢查 null
,你可能經(jīng)常會寫復雜的 if
或內嵌的 if
語句,就像下面這樣:
if (order != null) {
if (order.getCustomer() != null) {
if (order.getCustomer().getAddress() != null) {
System.out.println(order.getCustomer().getAddress());
}
}
}
利用 ?.
安全解除引用操作符,可以將上面的代碼利用下面的形式來簡化:
println order?.customer?.address
會在調用鏈中檢查 null 值,如果有元素為 null
,則不會拋出 NullPointerException
異常。如果有 元素為 null
,則結果值必為 null
。
可以使用 assert
語句來檢查參數(shù)、返回值以及更多類型的值。
與 Java 的 assert
有所不同,Groovy 的 assert
并不需要激活,它是一直被檢查的。
def check(String name) {
// 根據(jù) Groovy Truth,name 應為非 null 與非空
assert name
// 安全導航 + Groovy Truth
assert name?.size() > 3
}
另外要注意的是,Groovy 的 “強力斷言” 語句提供的輸出結果是很出色的,在生成的圖表中對每個子表達式的各種值都進行了斷言。
Elvis 操作符是一種特殊的三元操作符,對于處理默認值來說不啻是一種快捷方式。
我們往往會像下面這樣來書寫:
def result = name != null ? name : "Unknown"
多虧有了 Groovy Truth,null
檢查可以簡化為只用 name
就可以了。
進一步來說,既然要返回 name
,那么與其在這個三元表達式中重復兩次名稱,不如去掉問號和冒號之間的東西,使用 Elvis 操作符,可以這樣來完成:
def result = name ?: "Unknown"
如果不關心 try
語句塊中所要拋出的異常類型,可以只捕捉異常而忽略它們的類型。所以,像下面這樣的語句:
try {
// ...
} catch (Exception t) {
// 一些糟糕的事情
}
就可以變成下面這樣捕捉任何異常(any
或 all
都可以,只要是能讓你認為是任何東西的詞兒就可以用):
try {
// ...
} catch (any) {
// 一些糟糕的事情
}
它會捕捉所有異常,而并不僅是 Throwable
的異常。如果需要捕捉的是“每一個”異常,必須明確地聲明要捕捉的是 Throwable
異常。
最后講講什么時候以及如何使用可選類型。Groovy 允許自己決定是否使用顯式的強類型,或何時使用 def
。
簡單的經(jīng)驗法則是:如果你寫的代碼將被其他人用作公共 API,你就應該使用強類型,它能有助于合約的健壯性,避免可能通過的參數(shù)類型錯誤,形成更好的文檔,有助于 IDE 自動完成代碼。假如代碼只是自用,比如私有方法,或 IDE 能夠輕松地推斷類型,那么你就可以更自由地確定何時利用類型。