精通 Groovy
假設我將代碼保存在文件 MyFirstExample.groovy 內,只要輸入以下代碼就能運行這個示例:
| 1 | c:>groovy MyFirstExample.groovy |
在控制臺上輸出 “Hello World!” 所需的工作就這么多。
快捷方式應用
您可能注意到了,我不必編譯?.groovy?文件。這是因為 Groovy 屬于腳本語言。腳本語言的一個特點就是能夠在運行時進行解釋。(在 Java 中,要從源代碼編譯生成字節碼,然后才能進行解釋。區別在于腳本語言能夠直接?解釋源代碼。)
Groovy 允許完全省略編譯步驟,不過仍然可以?進行編譯。如果想要編譯代碼,可以使用 Groovy 編譯器?groovyc。用?groovyc?編譯 Groovy 代碼會產生標準的 Java 字節碼,然后可以通過?java?命令運行生成的字節碼。這是 Groovy 的一項經常被忽略的關鍵特性:用 Groovy 編寫的所有代碼都能夠通過標準 Java 運行時編譯和運行。
至于運行代碼,如果我希望更加簡潔,我甚至還能輸入
| 1 | c:>groovy -e "println 'Hello World!'" |
這會生成相同的結果,而且甚至無需定義任何文件!
Groovy 入門
在這一節中,將真正開始進行 Groovy 編程。首先,學習如何輕松地安裝 Groovy(通過 Eclipse Groovy 插件),然后從一些有助于了解 Groovy 的簡單示例開始。
輕松安裝 Groovy
為了迅速開始使用 Groovy,需要做的全部工作就是安裝 Eclipse 的 Groovy 插件。打開 Ecliplse,在?Help菜單中選擇?Software Updates?>?Find and Install...。
圖 1 顯示了執行以上步驟之后出現的對話框:
圖 1. Eclipse Feature Updates 對話框
在選項中導航
接下來,出現一個對話框,里面包含兩個選項。請選擇?Search for new features to install?單選按鈕。單擊?Next?按鈕,然后選擇?New Remote Site...。出現一個新的對話框,里面包含兩個需要填寫的字段:新位置的名稱和該位置的 URL,如圖 2 所示:
圖 2. 確保為新的遠程站點提供了正確的 URL
輸入 “Groovy plugin” 作為名稱,輸入 “http://dist.codehaus.org/groovy/distributions/update/” 作為位置,單擊?OK?按鈕,在隨后出現的?Sites to include in search?框中確保選中了名為 “Groovy plugin” 的項目 — 現在的列表應該如圖 3 所示。
圖 3.Eclipse 中的遠程網站清單
完成安裝
單擊?Finish?按鈕之后,應該會出現 Search Results 對話框。請再次確定選中了 “Groovy plugin” 框并單擊?Next?按鈕,這一步驟如圖 4 所示:
圖 4. 選擇 Groovy 插件
經過一系列確認之后,將會下載插件,然后可能需要重新啟動 Eclipse。
創建 Groovy 項目
Eclipse 重啟之后,就能夠創建第一個 Groovy 項目了。請確保創建兩個源文件夾 — 一個稱為 “groovy”,另一個稱為 “java”。編寫的 Groovy 代碼放在 groovy 文件夾,Java 代碼放在 java 文件夾。我發現將二者分開將會很有用,如圖 5 所示:
圖 5. 兩個源文件夾 — Java 和 Groovy
將 Groovy 導入項目
項目創建之后,右鍵單擊項目的圖標,應該會看到一個?Groovy?選項,如圖 6 所示。請選擇該選項,然后選擇?Add Groovy Nature?選項。這樣做可以將必要的 Groovy 庫、編譯器和運行程序導入到項目中。
圖 6. 在 Eclipse 中添加 Groovy 特性
創建 Groovy 類
創建 Groovy 類很簡單。選擇?groovy?文件夾并右鍵單擊它。選擇?New,然后選擇?Other,如圖 7 所示:
圖 7. 通過 New 菜單創建 Groovy 類
給類命名
在這里,找到 Groovy 文件夾,并選擇?Groovy Class— 應該會看到一個對話框,如圖 8 所示。
圖 8.選擇創建 Groovy 類
單擊?Next?按鈕,系統將要求您提供類的名稱。輸入?HelloWorld。
現在可以將?HelloWorld?Groovy 類保留在默認包內,如圖 9 所示。
圖 9. 現在不必考慮包的問題!
雖然步驟看起來很多,但這與創建標準的 Java 類并沒有什么區別。
Hello World! — 用 Groovy 編寫的 Java 程序
單擊?Finish?按鈕,應該會看到如下所示的代碼段:
| 1 2 3 4 5 | class HelloWorld { ?static void main(args) { ????????? ?} } |
這看起來同前面的 Java?HelloWorld?示例驚人地相似。但是請注意,它不包含?public?修改符。而且,如果仔細查看?main?方法的參數,會注意到它沒有類型。
編譯程序
現在在?main?方法內加入?println "Hello World",完成后的代碼看起來如下所示:
| 1 2 3 4 5 | class HelloWorld { ?static void main(args) { ???println "Hello World"??? ?} } |
在源代碼編輯器中應該能夠右鍵單擊,并選擇?Compile Groovy File?選項,如圖 10 所示。
圖 10. 右鍵單擊 Groovy 文件即可進行編譯
運行程序
接下來,再次右鍵單擊文件,選擇?Run As?選項,然后選擇?Groovy?選項。在 Eclipse 控制臺中應該會看到輸出的 “Hello World”,如圖 11 所示。
圖 11. 輸出的 Hello World
學到了什么?
OK,那么這是一種突出重點的取巧方式。Groovy?實際上就是?Java。其語法不同 — 多數情況下會短一些 — 但 Groovy 代碼 100% 符合 Java 字節碼標準。下一節將進一步介紹這兩種語言的交叉。
Groovy 變身為 Java
前面已經看到 Groovy 與 Java 代碼實際上可以互換的第一個證據。這一節將進一步證明這點,繼續使用 Groovy 構建的?HelloWorld?類。
Hello, Java!
為了使您確信 Groovy 就是 Java,現在在?HelloWorld?類聲明和方法聲明前面加上?public?修改符,如下所示:
| 1 2 3 4 5 | public class HelloWorld { ?public static void main(args) { ??println "Hello World" ?} } |
還不確信?
這個代碼運行起來同前面的代碼完全一樣。但是,如果仍不確信,還可以在?args?參數前加上?String[]:
| 1 2 3 4 5 | public class HelloWorld { ?public static void main(String[]args) { ??println "Hello World" ?} } |
現在還沒完
現在,還可以將?println?替換為?System.out.println— 而且不要忘記加上括號。
| 1 2 3 4 5 | public class HelloWorld { ?public static void main(String[] args) { ??System.out.println("Hello World") ??} } |
現在的代碼與前面用 Java 編寫的 Hello World 示例完全相同,但是哪個示例更容易編寫呢?
請注意,原來的基于 Groovy 的?HelloWorld?類沒有任何?public?修改符,沒有任何類型(沒有?String[]),而且提供了沒有括號的?println?快捷方式。
Hello, Groovy!
如果喜歡,可以將這個過程完全翻轉過來,回到基于 Java 的 Hello World 示例,刪除文件里的所有內容,只保留?System.out?行,然后在這行刪除?System.out?和括號。最后只剩下:
| 1 | println "Hello World" |
現在,哪個程序更容易編寫呢?
運行程序!
Groovy 代碼完全符合 Java 字節碼標準,這個練習證明了這一點。在 Eclipse 中,選擇?Run?菜單選項?Open Run Dialog...。選擇一個新的?Java Application?配置。確保項目是您的 Groovy 項目。對于?Main類,單擊?Search?按鈕,找到?HelloWorld?類。請注意,單詞?class?表明 Eclipse Groovy 插件已經將 .groovy 文件編譯為 .class 文件。
在圖 12 中可以看到整個這個過程 — 如果以前在 Eclipse 中運行過 Java 類,那么您應該對這個過程很熟悉。
圖 12. Groovy 代碼完全符合 Java 字節碼標準
單擊?Run?按鈕,看到什么了?實際上,“Hello World!” 從未像現在這樣能夠說明問題。
Groovy 是沒有類型的 Java 代碼
很可能將 Groovy 當成是沒有規則的 Java 代碼。但實際上,Groovy 只是規則少一些。這一節的重點是使用 Groovy 編寫 Java 應用程序時可以不用考慮的一個 Java 編程的具體方面:類型定義。
為什么要有類型定義?
在 Java 中,如果要聲明一個?String?變量,則必須輸入:
| 1 | String value = "Hello World"; |
但是,如果仔細想想,就會看出,等號右側的字符已經表明?value?的類型是?String。所以,Groovy 允許省略?value?前面的?String?類型變量,并用?def?代替。
| 1 | def value = "Hello World" |
實際上,Groovy 會根據對象的值來判斷它的類型。
運行程序!
將 HelloWorld.groovy 文件中的代碼編輯成下面這樣:
| 1 2 | String message = "Hello World" println message |
運行這段代碼,應該會在控制臺上看到與前面一樣的 “Hello World”。現在,將變量類型?String?替換為?def?并重新運行代碼。是不是注意到了相同的結果?
除了輸出?message?的值,還可以用以下調用輸出它的類型:
| 1 2 | def message = "Hello World" println message.class |
輸出 “class java.lang.String” 應該是目前為止很受歡迎的一項變化!如果想知道到底發生了什么,那么可以告訴您:Groovy 推斷出?message?一定是?String?類型的,因為它的值是用雙引號括起來的。
類型推斷的更多內容
您可能聽說過,在 Groovy 中 “一切都是對象” — 但對于類型來說這句話意味著什么呢?讓我們看看如果將前面示例中的?String?替換為數字會怎么樣,如下所示:
| 1 2 | def message = 12 println message.class |
message?變量的數字值看起來像是 Java 的原生類型?int。但是,運行這個代碼就可以看出,Groovy 將它作為?Integer。這是因為在 Groovy 中 “一切都是對象”。
Java 中的所有對象都擴展自?java.lang.Object,這對 Groovy 來說非常方便。即使在最糟的情況下,Groovy 運行時不能確定變量的類型,它只需將變量當成?Object,問題就解決了。
繼續使用這段代碼。將?message?改成自己喜歡的任意類型:Groovy 會在運行時盡其所能推斷出這個變量的類型。
無類型有什么意義
那么,Groovy 缺少類型意味著所需的輸入更少。不可否認,將?String?替換成?def?并沒有真正節約多少打字工作 — 三個字母并不值得如何夸耀!但是在更高的層次上看,在編寫大量不僅僅包含變量聲明的代碼的時候,沒有類型確實減少了許多打字工作。更重要的是,這意味著要閱讀的代碼要少得多。最后,Groovy 缺少類型能夠帶來更高的靈活性 — 不需要接口或抽象類。
所以,只需要使用?def?關鍵字就能在方法中聲明一個獨立變量,不需要將?def?關鍵字作為方法聲明中的參數。在?for?循環聲明中也不需要它,這意味著不用編寫?(int x = 0; x < 5; x++),相反,可以省略?int,保留空白。
通過 Groovy 進行循環
同大多數腳本語言一樣,Groovy 經常被宣傳為生產力更高?的 Java 語言替代品。您已經看到了 Groovy 缺少類型能夠如何減少打字工作。在這一節,將創建并試用一個?repeat?函數。在這個過程中,將進一步探索 Groovy 提高效率的方式。
更好、更短的循環
下面這種方法可以更好地感受 Groovy 缺乏類型的好處:首先,用與創建?HelloWorld?相同的方式創建一個 Groovy 類,將這個類稱為?MethodMadness,并刪除自動生成的類體:將要定義一個獨立的?repeat?函數。現在在控制臺中輸入以下代碼:
| 1 2 3 4 5 | def repeat(val){ ?for(i = 0; i < 5; i++){ ??println val ?} } |
起初,從 Java 的角度來看,這個小函數看起來可能有些怪(實際上,它很像 JavaScript)。但它就是 Java 代碼,只不過是用 Groovy 的樣式編寫的。
深入方法
repeat?函數接受一個變量?val。請注意參數不需要?def。方法體本質上就是一個?for?循環。
調用這個函數。
| 1 2 | repeat("hello ??world") |
會輸出 “hello world” 五次。請注意,for?循環中省略了?int。沒有變量類型的?for?循環要比標準的 Java 代碼短些。現在看看如果在代碼里加入范圍會出現什么情況。
Groovy 中的范圍
范圍?是一系列的值。例如 “0..4” 表明包含?整數 0、1、2、3、4。Groovy 還支持排除范圍,“0..<4” 表示 0、1、2、3。還可以創建字符范圍:“a..e” 相當于 a、b、c、d、e。“a..<e” 包括小于?e?的所有值。
循環范圍
范圍為循環帶來了很大的方便。例如,前面從 0 遞增到 4 的?for?循環如下所示:
| 1 | for(i = 0; i < 5; i++) |
范圍可以將這個?for?循環變得更簡潔,更易閱讀:
| 1 2 3 4 5 | def repeat(val){ ?for(i in 0..5){ ??println val ?} } |
設置范圍
如果運行這個示例,可能會注意到一個小問題:“Hello World” 輸出了六次而不是五次。這個問題有三種解決方法:
- 將包含的范圍限制到 4:
1
for(i in 0..4)
- 從 1 而不是 0 開始:
1
2
3
4
5
def repeat(val){
?for(i in 1..5){
??println val
?}
}
- 將范圍由包含改為排除:
1
2
3
4
5
def repeat(val){
?for(i in 0..<5){
??println val
?}
}
不論采用哪種方法,都會得到原來的效果 — 輸出 “Hello World” 五次。
默認參數值
現在已經成功地使用 Groovy 的范圍表達式縮短了?repeat?函數。但這個函數依然有些限制。如果想重復 “Hello World” 八次該怎么辦?如果想對不同的值重復不同次數 — 比如 “Hello World” 重復八次,“Goodbye Sunshine” 重復兩次,這時該怎么辦?
每次調用?repeat?時都要指定需要的重復次數的做法已經過時了,特別是在已經適應了默認行為(重復五次)的時候。
Groovy 支持默認參數值,可以在函數或方法的正式定義中指定參數的默認值。調用函數的程序可以選擇省略參數,使用默認值。
更加復雜的參數值
使用前面的?repeat?函數時,如果希望調用程序能夠指定重復值,可以像下面這樣編碼:
| 1 2 3 4 5 | def repeat(val, repeat=5){ ?for(i in 0..<repeat){ ??println val ?} } |
像下面這樣調用該函數:
| 1 2 3 | repeat("Hello World", 2) repeat("Goodbye sunshine", 4) repeat("foo") |
結果會輸出 “Hello World” 兩次,“Goodbye sunshine” 四次,“foo” 五次(默認次數)。
Groovy 集合
在 Groovy 提供的所有方便的快捷方式和功能中,最有幫助的一個可能就是內置的?集合。回想一下在 Java 編程中是如何使用集合的 — 導入?java.util?類,初始化集合,將項加入集合。這三個步驟都會增加不少代碼。
而 Groovy 可以直接在語言內使用集合。在 Groovy 中,不需要導入專門的類,也不需要初始化對象。集合是語言本身的本地成員。Groovy 也使集合(或者列表)的操作變得非常容易,為增加和刪除項提供了直觀的幫助。
可以將范圍當作集合
在前一節學習了如何用 Groovy 的范圍將循環變得更容易。范圍表達式 “0..4” 代表數字的集合— 0、1、2、3 和 4。為了驗證這一點,請創建一個新類,將其命名為?Ranger。保留類定義和?main?方法定義。但是這次添加以下代碼:
| 1 2 3 | def range = 0..4 println range.class assert range instanceof List |
請注意,assert?命令用來證明范圍是?java.util.List?的實例。接著運行這個代碼,證實該范圍現在是類型?List?的集合。
豐富的支持
Groovy 的集合支持相當豐富,而且美妙之處就在于,在 Groovy 的魔法背后,一切都是標準的 Java 對象。每個 Groovy 集合都是?java.util.Collection?或?java.util.Map?的實例。
前面提到過,Groovy 的語法提供了本地列表和映射。例如,請將以下兩行代碼添加到?Ranger?類中:
| 1 2 3 | def coll = ["Groovy", "Java", "Ruby"] assert? coll instanceof Collection assert coll instanceof ArrayList |
你將會注意到,coll?對象看起來很像 Java 語言中的數組。實際上,它是一個?Collection。要在普通的 Java 代碼中得到集合的相同實例,必須執行以下操作:
| 1 2 3 4 | Collection<String> coll = new ArrayList<String>(); coll.add("Groovy"); coll.add("Java"); coll.add("Ruby"); |
在 Java 代碼中,必須使用?add()?方法向?ArrayList?實例添加項。
添加項
Groovy 提供了許多方法可以將項添加到列表 — 可以使用?add()?方法(因為底層的集合是一個普通的?ArrayList?類型),但是還有許多快捷方式可以使用。
例如,下面的每一行代碼都會向底層集合加入一些項:
| 1 2 3 | coll.add("Python") coll << "Smalltalk" coll[5] = "Perl" |
請注意,Groovy 支持操作符重載 —<<?操作符被重載,以支持向集合添加項。還可以通過位置參數直接添加項。在這個示例中,由于集合中只有四個項,所以?[5]?操作符將 “Perl” 放在最后。請自行輸出這個集合并查看效果。
檢索非常輕松
如果需要從集合中得到某個特定項,可以通過像上面那樣的位置參數獲取項。例如,如果想得到第二個項 “Java”,可以編寫下面這樣的代碼(請記住集合和數組都是從 0 開始):
| 1 | assert coll[1] == "Java" |
Groovy 還允許在集合中增加或去掉集合,如下所示:
| 1 2 3 | def numbers = [1,2,3,4] assert numbers + 5 == [1,2,3,4,5] assert numbers - [2,3] == [1,4] |
請注意,在上面的代碼中, 實際上創建了新的?集合實例,由最后一行可以看出。
魔法方法
Groovy 還為集合添加了其他一些方便的功能。例如,可以在集合實例上調用特殊的方法,如下所示:
| 1 2 3 | def numbers = [1,2,3,4] assert numbers.join(",") == "1,2,3,4" assert [1,2,3,4,3].count(3) == 2 |
join()?和?count()?只是在任何項列表上都可以調用的眾多方便方法中的兩個。分布操作符(spread operator)?是個特別方便的工具,使用這個工具不用在集合上迭代,就能夠調用集合的每個項上的方法。
假設有一個?String?列表,現在想將列表中的項目全部變成大寫,可以編寫以下代碼:
| 1 2 | assert ["JAVA", "GROOVY"] == ??["Java", "Groovy"]*.toUpperCase() |
請注意?*.?標記。對于以上列表中的每個值,都會調用?toUpperCase(),生成的集合中每個?String?實例都是大寫的。
Groovy 映射
除了豐富的列表處理功能,Groovy 還提供了堅固的映射機制。同列表一樣,映射也是本地數據結構。而且 Groovy 中的任何映射機制在幕后都是?java.util.Map?的實例。
Java 語言中的映射
Java 語言中的映射是名稱-值對的集合。所以,要用 Java 代碼創建典型的映射,必須像下面這樣操作:
| 1 2 3 | Map<String, String>map = new HashMap<String, String>(); map.put("name", "Andy"); map.put("VPN-#","45"); |
一個?HashMap?實例容納兩個名稱-值對,每一個都是?String?的實例。
通過 Groovy 進行映射
Groovy 使得處理映射的操作像處理列表一樣簡單 — 例如,可以用 Groovy 將上面的 Java 映射寫成
| 1 | def hash = [name:"Andy", "VPN-#":45] |
請注意,Groovy 映射中的鍵不必是?String。在這個示例中,name?看起來像一個變量,但是在幕后,Groovy 會將它變成?String。
全都是 Java
接下來創建一個新類?Mapper?并加入上面的代碼。然后添加以下代碼,以證實底層的代碼是真正的 Java 代碼:
| 1 | assert hash.getClass() == java.util.LinkedHashMap |
可以看到 Groovy 使用了 Java 的?LinkedHashMap?類型,這意味著可以使用標準的 Java 一樣語句對?hash中的項執行?put?和?get?操作。
| 1 2 | hash.put("id", 23) assert hash.get("name") == "Andy" |
有 groovy 特色的映射
現在您已經看到,Groovy 給任何語句都施加了魔法,所以可以用?.?符號將項放入映射中。如果想將新的名稱-值對加入映射(例如?dob?和 “01/29/76”),可以像下面這樣操作:
| 1 | hash.dob = "01/29/76" |
.?符號還可以用來獲取項。例如,使用以下方法可以獲取?dob?的值:
| 1 | assert hash.dob == "01/29/76" |
當然?.?要比調用?get()?方法更具 Groovy 特色。
位置映射
還可以使用假的位置語法將項放入映射,或者從映射獲取項目,如下所示:
| 1 2 3 4 | assert hash["name"] == "Andy" hash["gender"] = "male" assert hash.gender == "male" assert hash["gender"] == "male" |
但是,請注意,在使用?[]?語法從映射獲取項時,必須將項作為?String?引用。
Groovy 中的閉包
現在,閉包是 Java 世界的一個重大主題,對于是否會在 Java 7 中包含閉包仍然存在熱烈的爭論。有些人會問:既然 Groovy 中已經存在閉包,為什么 Java 語言中還需要閉包?這一節將學習 Groovy 中的閉包。如果沒有意外,在閉包成為 Java 語法的正式部分之后,這里學到的內容將給您帶來方便。
不再需要更多迭代
雖然在前幾節編寫了不少集合代碼,但還沒有實際地在集合上迭代。當然,您知道 Groovy 就是 Java,所以如果愿意,那么總是能夠得到 Java 的?Iterator?實例,用它在集合上迭代,就像下面這樣:
| 1 2 3 4 5 | def acoll = ["Groovy", "Java", "Ruby"] ????????? for(Iterator iter = acoll.iterator(); iter.hasNext();){ ?println iter.next() } |
實際上在?for?循環中并不需要類型聲明,因為 Groovy 已經將迭代轉變為任何集合的直接成員。在這個示例中,不必獲取?Iterator?實例并直接操縱它,可以直接在集合上迭代。而且,通常放在循環構造內的行為(例如?for?循環體中?println)接下來要放在閉包內。在深入之前,先看看如何執行這步操作。
能否看見閉包?
對于上面的代碼,可以用更簡潔的方式對集合進行迭代,如下所示:
| 1 2 3 4 5 | def acoll = ["Groovy", "Java", "Ruby"] ????????? acoll.each{ ?println it } |
請注意,each?直接在?acoll?實例內調用,而?acoll?實例的類型是?ArrayList。在?each?調用之后,引入了一種新的語法 —{,然后是一些代碼,然后是?}。由?{}?包圍起來的代碼塊就是閉包。
執行代碼
閉包是可執行的代碼塊。它們不需要名稱,可以在定義之后執行。所以,在上面的示例中,包含輸出?it(后面將簡單解釋?it)的行為的無名閉包將會在?acoll?集合類型中的每個值上被調用。
在較高層面上,{}?中的代碼會執行三次,從而生成如圖 13 所示的輸出。
圖 13. 迭代從未像現在這樣容易
閉包中的?it?變量是一個關鍵字,指向被調用的外部集合的每個值 — 它是默認值,可以用傳遞給閉包的參數覆蓋它。下面的代碼執行同樣的操作,但使用自己的項變量:
| 1 2 3 4 5 | def acoll = ["Groovy", "Java", "Ruby"] ????????? acoll.each{ value -> ?println value } |
在這個示例中,用?value?代替了 Groovy 的默認?it。
迭代無處不在
閉包在 Groovy 中頻繁出現,但是,通常用于在一系列值上迭代的時候。請記住,一系列值可以用多種方式表示,不僅可以用列表表示 — 例如,可以在映射、String、JDBC?Rowset、File?的行上迭代,等等。
如果想在前面一節 “Groovy 中的映射” 中的?hash?對象上迭代,可以編寫以下代碼:
| 1 2 3 4 | def hash = [name:"Andy", "VPN-#":45] hash.each{ key, value -> ?println "${key} : ${value}" } |
請注意,閉包還允許使用多個參數 — 在這個示例中,上面的代碼包含兩個參數(key?和?value)。
使用 Java 代碼迭代
以下是使用典型的 Java 構造如何進行同樣的迭代:
| 1 2 3 4 5 6 7 8 9 | Map<String, String>map = new HashMap<String, String>(); map.put("name", "Andy"); map.put("VPN-#","45"); ????????? ????????? for(Iterator iter = map.entrySet().iterator(); iter.hasNext();){ ?Map.Entry entry = (Map.Entry)iter.next(); ?System.out.println(entry.getKey() + " : " + entry.getValue()); } |
上面的代碼比 Groovy 的代碼長得多,是不是?如果要處理大量集合,那么顯然用 Groovy 處理會更方便。
迭代總結
請記住,凡是集合或一系列的內容,都可以使用下面這樣的代碼進行迭代。
| 1 2 3 | "ITERATION".each{ ?println it.toLowerCase() } |
閉包的更多使用方式
雖然在迭代上使用閉包的機會最多,但閉包確實還有其他用途。因為閉包是一個代碼塊,所以能夠作為參數進行傳遞(Groovy 中的函數或方法不能這樣做)。閉包在調用的時候才會執行這一事實(不是在定義的時候)使得它們在某些場合上特別有用。
例如,通過 Eclipse 創建一個?ClosureExample?對象,并保持它提供的默認類語法。在生成的?main()?方法中,添加以下代碼:
| 1 2 3 | def excite = { word -> ?return "${word}!!" } |
這段代碼是名為?excite?的閉包。這個閉包接受一個參數(名為?word),返回的?String?是?word?變量加兩個感嘆號。請注意在?String?實例中替換?的用法。在?String?中使用?${value}語法將告訴 Groovy 替換?String?中的某個變量的值。可以將這個語法當成?return word + "!!"?的快捷方式。
延遲執行
既然有了閉包,下面就該實際使用它了。可以通過兩種方法調用閉包:直接調用或者通過?call()?方法調用。
繼續使用?ClosureExample?類,在閉包定義下面添加以下兩行代碼:
| 1 2 | assert "Groovy!!" == excite("Groovy") assert "Java!!" == excite.call("Java") |
可以看到,兩種調用方式都能工作,但是直接調用的方法更簡潔。不要忘記閉包在 Groovy 中也是一類對象 — 既可以作為參數傳遞,也可以放在以后執行。用普通的 Java 代碼可以復制同樣的行為,但是不太容易。現在不會感到驚訝了吧?
Groovy 中的類
迄今為止,您已經用 Groovy 輸出了許多次 “Hello World”,已經操作了集合,用閉包在集合上迭代,也定義了您自己的閉包。做所有這些工作時,甚至還沒有討論那個對 Java 開發人員來說至關重要的概念 — 類。
當然,您已經在這個教程中使用過類了:您編寫的最后幾個示例就是在不同類的?main()?方法中。而且,您已經知道,在 Groovy 中可以像在 Java 代碼中一樣定義類。惟一的區別是,不需要使用?public?修改符,而且還可以省略方法參數的類型。這一節將介紹使用 Groovy 類能夠進行的其他所有操作。
Song 類
我們先從用 Groovy 定義一個簡單的 JavaBean 形式的類開始,這個類稱為?Song。
第一步自然是用 Groovy 創建名為?Song?的類。這次還要為它創建一個包結構 — 創建一個包名,例如?org.acme.groovy。
創建這個類之后,刪除 Groovy 插件自動生成的?main()。
歌曲有一些屬性 — 創作歌曲的藝術家、歌曲名稱、風格等等。請將這些屬性加入新建的?Song?類,如下所示:
| 1 2 3 4 5 6 7 | package org.acme.groovy ? class Song { ?def name ?def artist ?def genre } |
迄今為止還不錯,是不是?對于 Grooovy 的新開發人員來說,還不算太復雜!
Groovy 類就是 Java 類
應該還記得本教程前面說過 Groovy 編譯器為用 Groovy 定義的每個類都生成標準的 Java?.class。還記得如何用 Groovy 創建?HelloWorld?類、找到?.class?文件并運行它么?也可以用新定義的?Song?類完成同樣的操作。如果通過 Groovy 的?groovyc?編譯器編譯代碼(Eclipse Groovy 插件已經這樣做了),就會生成一個?Song.class?文件。
這意味著,如果想在另一個 Groovy 類或 Java 類中使用新建的?Song?類,則必須導入?它(當然,除非使用?Song?的代碼與?Song?在同一個包內)。
接下來創建一個新類,名為?SongExample,將其放在另一個包結構內,假設是?org.thirdparty.lib。
現在應該看到如下所示的代碼:
| 1 2 3 4 5 | package org.thirdparty.lib ? class SongExample { ?static void main(args) {} } |
類的關系
現在是使用?Song?類的時候了。首先導入實例,并將下面的代碼添加到?SongExample?的?main()?方法中。
| 1 2 3 4 5 6 7 8 9 10 | package org.thirdparty.lib ? import org.acme.groovy.Song ? class SongExample { ?static void main(args) { ??def sng = new Song(name:"Le Freak", ????artist:"Chic", genre:"Disco") ?} } |
現在?Song?實例創建完成了!但是仔細看看以前定義的?Song?類的初始化代碼,是否注意到什么特殊之處?您應該注意到自動生成了構造函數。
類初始化
Groovy 自動提供一個構造函數,構造函數接受一個名稱-值對的映射,這些名稱-值對與類的屬性相對應。這是 Groovy 的一項開箱即用的功能 — 用于類中定義的任何屬性,Groovy 允許將存儲了大量值的映射傳給構造函數。映射的這種用法很有意義,例如,您不用初始化對象的每個屬性。
也可以添加下面這樣的代碼:
| 1 | def sng2 = new Song(name:"Kung Fu Fighting", genre:"Disco") |
也可以像下面這樣直接操縱類的屬性:
| 1 2 3 4 5 6 | def sng3 = new Song() sng3.name = "Funkytown" sng3.artist = "Lipps Inc." sng3.setGenre("Disco") ????????? assert sng3.getArtist() == "Lipps Inc." |
從這個代碼中明顯可以看出,Groovy 不僅創建了一個構造函數,允許傳入屬性及其值的映射,還可以通過?.?語法間接地訪問屬性。而且,Groovy 還生成了標準的 setter 和 getter 方法。
在進行屬性操縱時,非常有 Groovy 特色的是:總是會調用 setter 和 getter 方法 — 即使直接通過?.?語法訪問屬性也是如此。
核心的靈活性
Groovy 是一種本質上就很靈活的語言。例如,看看從前面的代碼中將?setGenre()?方法調用的括號刪除之后會怎么樣,如下所示:
| 1 2 | sng3.setGenre "Disco" assert sng3.genre == "Disco" |
在 Groovy 中,對于接受參數的方法,可以省略括號 — 在某些方面,這樣做會讓代碼更容易閱讀。
方法覆蓋
迄今為止已經成功地創建了?Song?類的一些實例。但是,它們還沒有做什么有趣的事情。可以用以下命令輸出一個實例:
| 1 | println sng3 |
在 Java 中這樣只會輸出所有對象的默認?toString()?實現,也就是類名和它的 hashcode(即?org.acme.groovy.Song@44f787)。下面來看看如何覆蓋默認的?toString()?實現,讓輸出效果更好。
在?Song?類中,添加以下代碼:
| 1 2 3 | String toString(){ ?"${name}, ${artist}, ${genre}" } |
根據本教程已經學到的內容,可以省略?toString()?方法上的?public?修改符。仍然需要指定返回類型(String),以便實際地覆蓋正確的方法。方法體的定義很簡潔 — 但?return?語句在哪?
不需要 return
您可能已經想到:在 Groovy 中可以省略?return?語句。Groovy 默認返回方法的最后一行。所以在這個示例中,返回包含類屬性的?String。
重新運行?SongExample?類,應該會看到更有趣的內容。toString() 方法返回一個描述,而不是 hashcode。
特殊訪問
Groovy 的自動生成功能對于一些功能來說很方便,但有些時候需要覆蓋默認的行為。例如,假設需要覆蓋?Song?類中?getGenre()?方法,讓返回的?String?全部為大寫形式。
提供這個新行為很容易,只要定義?getGenre()?方法即可。可以讓方法的聲明返回?String,也可以完全省略它(如果愿意)。下面的操作可能是最簡單的:
| 1 2 3 | def getGenre(){ ?genre.toUpperCase() } |
同以前一樣,這個簡單方法省略了返回類型和?return?語句。現在再次運行?SongExample?類。應該會看到一些意外的事情 —— 出現了空指針異常。
空指針安全性
如果您一直在跟隨本教程,那么應該已經在?SongExample?類中加入了下面的代碼:
| 1 | assert sng3.genre == "Disco" |
結果在重新運行?SongExample?時出現了斷言錯誤 — 這正是為什么在 Eclipse 控制臺上輸出了丑陋的紅色文字。(很抱歉使用了這么一個糟糕的技巧)
幸運的是,可以輕松地修復這個錯誤:只要在?SongExample?類中添加以下代碼:
| 1 | println sng2.artist.toUpperCase() |
但是現在控制臺上出現了更多的?紅色文本 — 出什么事了?!
可惡的 null
如果回憶一下,就會想起?sng2?實例沒有定義?artist?值。所以,在調用?toUpperCase()?方法時就會生成?Nullpointer?異常。
幸運的是, Groovy 通過???操作符提供了一個安全網 — 在方法調用前面添加一個???就相當于在調用前面放了一個條件,可以防止在 null 對象上調用方法。
例如,將?sng2.artist.toUpperCase()?行替換成?sng2.artist?.toUpperCase()。請注意,也可以省略后面的括號。(Groovy 實際上也允許在不帶參數的方法上省略括號。不過,如果 Groovy 認為您要訪問類的屬性而不是方法,那么這樣做可能會造成問題。)
重新運行?SongExample?類,您會發現???操作符很有用。在這個示例中,沒有出現可惡的異常。現在將下面的代碼放在這個類內,再次運行代碼。
| 1 2 | def sng4 = new Song(name:"Thriller", artist:"Michael Jackson") println sng4 |
就是 Java
您將會注意到,雖然預期可能有異常,但是沒有生成異常。即使沒有定義?genre,getGenre()?方法也會調用?toUpperCase()。
您還記得 Groovy 就是 Java,對吧?所以在?Song?的?toString()?中,引用了?genre?屬性本身,所以不會調用?getGenre()。現在更改?toString()?方法以使用?getGenre(),然后再看看程序運行的結果。
| 1 2 3 | String toString(){ ?"${name}, ${artist}, ${getGenre()}" } |
重新運行?SongExample,出現類似的異常。現在,請自己嘗試修復這個問題,看看會發生什么。
另一個方便的小操作符
希望您做的修改與我的類似。在下面將會看到,我進一步擴充了?Song?類的?getGenre()?方法,以利用 Groovy 中方便的???操作符。
| 1 2 3 | def getGenre(){ ?genre?.toUpperCase() } |
??操作符時刻都非常有用,可以極大地減少條件語句。
對 Groovy 進行單元測試
本教程一直都強調 Groovy 只是 Java 的一個變體。您已經看到可以用 Groovy 編寫并使用標準的 Java 程序。為了最后一次證明這點,在結束本教程之前,我們將通過 JUnit?利用 Java?對?Song?類進行單元測試。
將 JUnit 加入 Eclipse 項目
為了跟上本節的示例,需要將 JUnit 加入到 Eclipse 項目中。首先,右鍵單擊項目,選擇?Build Path,然后選擇?Add Libraries,如圖 14 所示:
圖 14. 將 JUnit 加入到項目的構建路徑
會出現?Add Library?對話框,如圖 15 所示。
圖 15. 從庫列表中選擇 JUnit
選擇 JUnit 并單擊?Next?按鈕。應該會看到如圖 16 所示的對話框。選擇?JUnit3?或?4— 具體選擇哪項全憑自己決定 — 并單擊?Finish?按鈕。
圖 16. 選擇 JUnit 3 或 JUnit 4
設置新的測試用例
現在在項目的類路徑中加入了 JUnit,所以能夠編寫 JUnit 測試了。請右鍵單擊 java 源文件夾,選擇?New,然后選擇?JUnit Test Case。定義一個包,給測試用例命名(例如?SongTest),在 Class Under Test 部分,單擊?Browse?按鈕。
請注意,可以選擇用 Groovy 定義的?Song?類。圖 17 演示了這一步驟:
圖 17.找到 Song 類
選擇該類并單擊?OK(應該會看到與圖 18 類似的對話框)并在 New JUnit Test Case 對話框中單擊?Finish?按鈕。
圖 18. Song 的新測試用例
定義測試方法
我選擇使用 JUnit 4;所以我定義了一個名為?testToString()?的測試方法,如下所示:
| 1 2 3 4 5 6 7 8 9 10 | package org.acme.groovy; ? import org.junit.Test; ? public class SongTest { ????? ?@Test ?public void testToString(){} ? } |
測試 toString
顯然,需要驗證?toString()?方法是否沒有問題,那么第一步該做什么呢?如果想的是 “導入?Song?類”,那么想得就太難了 —Song?類在同一個包內,所以第一步是創建它的實例。
在創建用于測試的?Song?實例時,請注意不能通過傳給構造函數的映射完全初始化 — 而且,如果想自動完成實例的 setter 方法,可以看到每個 setter 接受的是?Object?而不是?String(如圖 19 所示)。為什么會這樣呢?
圖 19. 所有的 setter 和 getter
Groovy 的功勞
如果回憶一下,就會記得我在本教程開始的時候說過:
因為 Java 中的每個對象都擴展自?java.lang.Object,所以即使在最壞情況下,Groovy 不能確定變量的類型,Groovy 也能將變量的類型設為?Object然后問題就會迎刃而解。現在回想一下,在定義?Song?類時,省略了每個屬性的類型。Groovy 將自然地將每個屬性的類型設為?Object。所以,在標準 Java 代碼中使用?Song?類時,看到的 getter 和 setter 的參數類型和返回類型全都是?Object。
修正返回類型
為了增添樂趣,請打開 Groovy?Song?類,將?artist?屬性改為?String?類型,而不是無類型,如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package org.acme.groovy ? class Song { ?def name ?String artist ?def genre ????? ?String toString(){ ??"${name}, ${artist}, ${getGenre()}" ?} ????? ?def getGenre(){ ??genre?.toUpperCase() ?} } |
現在,回到 JUnit 測試,在?Song?實例上使用自動完成功能 — 看到了什么?
在圖 20 中(以及您自己的代碼中,如果一直跟隨本教程的話),setArtist()?方法接受一個?String,而不是Object。Groovy 再次證明了它就是 Java,而且應用了相同的規則。
圖 20. String,而不是 object
始終是普通的 Java
返回來編寫測試,另外請注意,默認情況下 Groovy 編譯的類屬性是私有的,所以不能直接在 Java 中訪問它們,必須像下面這樣使用 setter:
| 1 2 3 4 5 6 7 8 9 10 | @Test public void testToString(){ ?Song sng = new Song(); ?sng.setArtist("Village People"); ?sng.setName("Y.M.C.A"); ?sng.setGenre("Disco"); ????????? ?Assert.assertEquals("Y.M.C.A, Village People, DISCO", ???sng.toString()); } |
編寫這個測試用例余下的代碼就是小菜一碟了。測試用例很好地演示了這樣一點:用 Groovy 所做的一切都可以輕易地在 Java 程序中重用,反之亦然。用 Java 語言執行的一切操作和編寫的一切代碼,在 Groovy 中也都可以使用。
結束語
如果說您從本教程獲得了一個收獲的話(除了初次體驗 Groovy 編程之外),那么這個收獲應該是深入地認識到 Groovy 就是 Java,只是缺少了您過去使用的許多語法規則。Groovy 是沒有類型、沒有修改符、沒有 return、沒有?Iterator、不需要導入集合的 Java。簡而言之,Groovy 就是丟掉了許多包袱的 Java,這些包袱可能會壓垮 Java 項目。
但是在幕后,Groovy 就是 Java。
我希望通向精通 Groovy 的這第一段旅程給您帶來了快樂。您學習了 Groovy 語法,創建了幾個能夠體驗到 Groovy 的生產力增強功能的類,看到了用 Java 測試 Groovy 類有多容易。還遇到了第一次使用 Groovy 的開發者常見的一些問題,看到了如何在不引起太多麻煩的情況下解決它們。
盡管您可能覺得自己目前對 Groovy 還不是很熟練,但您已經走出了第一步。您可以用目前學到的知識編寫自己的第一個 Groovy 程序 — 畢竟,您已經設置好了同時支持 Groovy 和 Java 編程的雙重環境!作為有趣的練習,您可以試試用?Gant?設置下一個的自動構建版本,Gant 是基于 Ant 的構建工具,使用 Groovy 來定義構建,而不是使用 XML。當您對 Groovy 更加適應時,可以試著用 Groovy on?Grails?構建 Web 應用程序模塊 — 順便說一下,這是下一篇教程的主題。
相關主題
- “精通 Grails:構建您的第一個 Grails 應用程序”(Scott Davis,developerWorks,2006 年 1 月):了解 Grails,Grail 是一個現代的 Web 開發框架,能夠將遺留 Java 代碼與 Groovy 的靈活性和動態性無縫集成在一起。
- “輕量級開發的成功秘訣,第 7 部分: Java 替代方案”(Bruce Tate,developerWorks, 2005 年 9 月):這篇文章在較高層次上討論了閉包、延續、反射和元編程,從中可以了解什么因素會影響到語言的生產力。
- “What is new in Groovy 1.5”(InfoQ,Guillaume Laforge,2007 年 12 月有):Groovy 的項目經理重點講述了 Groovy 中新增的對 Java 5 注釋、泛型和枚舉的支持。
- “Groovy-power automated builds with Gant”(Klaus Berg,JavaWorld,2008 年 2 月):了解為什么有些 Java 開發人員選擇 Gant 作為更具表現力的替代方案來處理復雜構建。
- Disco Blog 中的 Groovy 文章(Andrew Glover,thediscoblog.com):Andrew 在這里針對 Groovy 的各個主題寫作了大量文章,例如單元測試、元編程和高級 Groovy 開發技術。
- IBM developer kit for Java technology 1.5.0 SR3:對在 Java 平臺上進行 Groovy 開發的強大支持。
- Sun JDK 1.5 或更新版本:要跟隨本教程中的示例,至少需要 1.5.0_09 版。
- Eclipse IDE:使用 Eclipse Groovy 插件輕松設置 Groovy 開發環境。
- JUnit:與 JUnit 3 或 JUnit 4 兼容的示例。
?
from:?https://www.ibm.com/developerworks/cn/education/java/j-groovy/j-groovy.html
總結
- 上一篇: JVM 的 工作原理,层次结构 以及 G
- 下一篇: 《Groovy官方指南》目录