簡介:在這一期的 實戰 Groovy 中,Scott Davis 提出了一組非常好的遍歷方法,這些方法可以遍 歷數組、列表、文件、URL 以及很多其它內容。最令人印象深刻的是,Groovy 提供了一種一致的機制來 遍歷所有這些集合和其它內容。
迭代是編程的基礎。您經常會遇到需要進行逐項遍歷的內容,比如 List、File 和 JDBC ResultSet 。Java 語言幾乎總是提供了某種方法幫助您逐項遍歷所需的內容,但令人沮喪的是,它並沒有給出一種 標准方法。Groovy 的迭代方法非常實用,在這一點上,Groovy 編程與 Java 編程截然不同。通過一些 代碼示例(可從 下載 小節獲得),本文將介紹 Groovy 的萬能的 each() 方法,從而將 Java 語言的 那些迭代怪癖拋在腦後。
Java 迭代策略
假設您有一個 Java 編程語言的 java.util.List。清單 1 展示了在 Java 語言中如何使用編程實現 迭代:
清單 1. Java 列表迭代
import java.util.*; public class ListTest{ public static void main(String[] args){ List<String> list = new ArrayList<String>(); list.add("Java"); list.add("Groovy"); list.add("JavaScript"); for(Iterator<String> i = list.iterator(); i.hasNext();){ String language = i.next(); System.out.println("I know " + language); } } }
由於提供了大部分集合類都可以共享的 java.lang.Iterable 接口,您可以使用相同的方法遍歷 java.util.Set 或 java.util.Queue。
關於本系列
Groovy 是一款運行在 Java 平台之上的現代編程語言。它能夠與現有 Java 代碼無縫集成,同時引 入了閉包和元編程等出色的新特性。簡而言之,Groovy 類似於 21 世紀的 Java 語言。
如果要將新工具集成到開發工具箱中,最關鍵的是理解什麼時候需要使用它以及什麼時候不適合使用 它。Groovy 可以變得非常強大,但前提是它被適當地應用到合適的場景中。因此, 實戰 Groovy 系列 旨在展示 Groovy 的實際使用,以及何時和如何成功應用它。
現在,假設該語言存儲在 java.util.Map 中。在編譯時,嘗試對 Map 獲取 Iterator 會導致失敗 — Map 並沒有實現 Iterable 接口。幸運的是,可以調用 map.keySet() 返回一個 Set,然後就可以繼 續處理。這些小差異可能會影響您的速度,但不會妨礙您的前進。需要注意的是,List、Set 和 Queue 實現了 Iterable,但是 Map 沒有 — 即使它們位於相同的 java.util 包中。
現在假設該語言存在於 String 數組中。數組是一種數據結構,而不是類。不能對 String 數組調用 .iterator(),因此必須使用稍微不同的迭代策略。您再一次受到阻礙,但可以使用如清單 2 所示的方 法解決問題:
清單 2. Java 數組迭代
public class ArrayTest{ public static void main(String[] args){ String[] list = {"Java", "Groovy", "JavaScript"}; for(int i = 0; i < list.length; i++){ String language = list[i]; System.out.println("I know " + language); } } }
但是等一下 — 使用 Java 5 引入的 for-each 語法怎麼樣(參見 參考資料)?它可以處理任何實 現 Iterable 的類和數組,如清單 3 所示:
清單 3. Java 語言的 for-each 迭代
import java.util.*; public class MixedTest{ public static void main(String[] args){ List<String> list = new ArrayList<String>(); list.add("Java"); list.add("Groovy"); list.add("JavaScript"); for(String language: list){ System.out.println("I know " + language); } String[] list2 = {"Java", "Groovy", "JavaScript"}; for(String language: list2){ System.out.println("I know " + language); } } }
因此,您可以使用相同的方法遍歷數組和集合(Map 除外)。但是如果語言存儲在 java.io.File, 那該怎麼辦?如果存儲在 JDBC ResultSet,或者存儲在 XML 文檔、java.util.StringTokenizer 中呢 ?面對每一種情況,必須使用一種稍有不同的迭代策略。這樣做並不是有什麼特殊目的 — 而是因為不 同的 API 是由不同的開發人員在不同的時期開發的 — 但事實是,您必須了解 6 個 Java 迭代策略, 特別是使用這些策略的特殊情況。
Eric S. Raymond 在他的 The Art of Unix Programming(參見 參考資料)一書中解釋了 “最少意 外原則”。他寫道,“要設計可用的接口,最好不要設計全新的接口模型。新鮮的東西總是難以入門; 會為用戶帶來學習的負擔,因此應當盡量減少新內容。”Groovy 對迭代的態度正是采納了 Raymond 的 觀點。在 Groovy 中遍歷幾乎任何結構時,您只需要使用 each() 這一種方法。
Groovy 中的列表迭代
首先,我將 清單 3 中的 List 重構為 Groovy。在這裡,只需要直接對列表調用 each() 方法並傳 遞一個閉包,而不是將 List 轉換成 for 循環(順便提一句,這樣做並不是特別具有面向對象的特征, 不是嗎)。
創建一個名為 listTest.groovy 的文件並添加清單 4 中的代碼:
清單 4. Groovy 列表迭代
def list = ["Java", "Groovy", "JavaScript"] list.each{language-> println language }
清單 4 中的第一行是 Groovy 用於構建 java.util.ArrayList 的便捷語法。可以將 println list.class 添加到此腳本來驗證這一點。接下來,只需對列表調用 each(),並在閉包體內輸出 language 變量。在閉包的開始處使用 language-> 語句命名 language 變量。如果沒有提供變量名 ,Groovy 提供了一個默認名稱 it。在命令行提示符中輸入 groovy listTest 運行 listTest.groovy。
清單 5 是經過簡化的 清單 4 代碼版本:
清單 5. 使用 Groovy 的 it 變量的迭代
// shorter, using the default it variable def list = ["Java", "Groovy", "JavaScript"] list.each{ println it } // shorter still, using an anonymous list ["Java", "Groovy", "JavaScript"].each{ println it }
Groovy 允許您對數組和 List 交替使用 each() 方法。為了將 ArrayList 改為 String 數組,必須 將 as String[] 添加到行末,如清單 6 所示:
清單 6. Groovy 數組迭代
def list = ["Java", "Groovy", "JavaScript"] as String[] list.each{println it}
在 Groovy 中普遍使用 each() 方法,並且 getter 語法非常便捷(getClass() 和 class 是相同的 調用),這使您能夠編寫既簡潔又富有表達性的代碼。例如,假設您希望利用反射顯示給定類的所有公 共方法。清單 7 展示了這個例子:
清單 7. Groovy 反射
def s = "Hello World" println s println s.class s.class.methods.each{println it} //output: $ groovy reflectionTest.groovy Hello World class java.lang.String public int java.lang.String.hashCode() public volatile int java.lang.String.compareTo(java.lang.Object) public int java.lang.String.compareTo(java.lang.String) public boolean java.lang.String.equals(java.lang.Object) ...
腳本的最後一行調用 getClass() 方法。java.lang.Class 提供了一個 getMethods() 方法,後者返 回一個數組。通過將這些操作串連起來並對 Method 的結果數組調用 each(),您只使用了一行代碼就完 成了大量工作。
但是,與 Java for-each 語句不同的是,萬能的 each() 方法並不僅限於 List 和數組。在 Java 語言中,故事到此結束。然而,在 Groovy 中,故事才剛剛開始。
Map 迭代
從前文可以看到,在 Java 語言中,無法直接迭代 Map。在 Groovy 中,這完全不是問題,如清單 8 所示:
清單 8. Groovy map 迭代
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] map.each{ println it }
要處理名稱/值對,可以使用隱式的 getKey() 和 getValue() 方法,或在包的開頭部分顯式地命名 變量,如清單 9 所示:
清單 9. 從 map 獲得鍵和值
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] map.each{ println it.key println it.value } map.each{k,v-> println k println v }
可以看到,迭代 Map 和迭代其它任何集合一樣自然。
在繼續研究下一個迭代例子前,應當了解 Groovy 中有關 Map 的另一個語法。與在 Java 語言中調 用 map.get("Java") 不一樣,可以簡化對 map.Java 的調用,如清單 10 所示:
清單 10. 獲得 map 值
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] //identical results println map.get("Java") println map.Java
不可否認,Groovy 針對 Map 的這種便捷語法非常酷,但這也是在對 Map 使用反射時引起一些常見 問題的原因。對 list.class 的調用將生成 java.util.ArrayList,而調用 map.class 返回 null。這 是因為獲得 map 元素的便捷方法覆蓋了實際的 getter 調用。Map 中的元素都不具有 class 鍵,因此 調用實際會返回 null,如清單 11 的示例所示:
清單 11. Groovy map 和 null
def list = ["Java", "Groovy", "JavaScript"] println list.class // java.util.ArrayList def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] println map.class // null map.class = "I am a map element" println map.class // I am a map element println map.getClass() // class java.util.LinkedHashMap
這是 Groovy 比較罕見的打破 “最少意外原則” 的情況,但是由於從 map 獲取元素要比使用反射 更加常見,因此我可以接受這一例外。
String 迭代
現在您已經熟悉 each() 方法了,它可以出現在所有相關的位置。假設您希望迭代一個 String,並 且是逐一迭代字符,那麼馬上可以使用 each() 方法。如清單 12 所示:
清單 12. String 迭代
def name = "Jane Smith" name.each{letter-> println letter }
這提供了所有的可能性,比如使用下劃線替代所有空格,如清單 13 所示:
清單 13. 使用下劃線替代空格
def name = "Jane Smith" println "replace spaces" name.each{ if(it == " "){ print "_" }else{ print it } } // output Jane_Smith
當然,在替換一個單個字母時,Groovy 提供了一個更加簡潔的替換方法。您可以將清單 13 中的所 有代碼合並為一行代碼:"Jane Smith".replace(" ", "_")。但是對於更復雜的 String 操作,each() 方法是最佳選擇。
Range 迭代
Groovy 提供了原生的 Range 類型,可以直接迭代。使用兩個點分隔的所有內容(比如 1..10)都是 一個 Range。清單 14 展示了這個例子:
清單 14. Range 迭代
def range = 5..10 range.each{ println it } //output: 5 6 7 8 9 10
Range 不局限於簡單的 Integer。考慮清單 15 在的代碼,其中迭代 Date 的 Range:
清單 15. Date 迭代
def today = new Date() def nextWeek = today + 7 (today..nextWeek).each{ println it } //output: Thu Mar 12 04:49:35 MDT 2009 Fri Mar 13 04:49:35 MDT 2009 Sat Mar 14 04:49:35 MDT 2009 Sun Mar 15 04:49:35 MDT 2009 Mon Mar 16 04:49:35 MDT 2009 Tue Mar 17 04:49:35 MDT 2009 Wed Mar 18 04:49:35 MDT 2009 Thu Mar 19 04:49:35 MDT 2009
可以看到,each() 准確地出現在您所期望的位置。Java 語言缺乏原生的 Range 類型,但是提供了 一個類似地概念,采取 enum 的形式。毫不奇怪,在這裡 each() 仍然派得上用場。
Enumeration 類型
Java enum 是按照特定順序保存的隨意的值集合。清單 16 展示了 each() 方法如何自然地配合 enum,就好象它在處理 Range 操作符一樣:
清單 16. enum 迭代
enum DAY{ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } DAY.each{ println it } (DAY.MONDAY..DAY.FRIDAY).each{ println it }
在 Groovy 中,有些情況下,each() 這個名稱遠未能表達它的強大功能。在下面的例子中,將看到 使用特定於所用上下文的方法對 each() 方法進行修飾。Groovy eachRow() 方法就是一個很好的例子。
SQL 迭代
在處理關系數據庫表時,經常會說 “我需要針對表中的每一行執行操作”。比較一下前面的例子。 您很可能會說 “我需要對列表中的每一種語言執行一些操作”。根據這個道理,groovy.sql.Sql 對象 提供了一個 eachRow() 方法,如清單 17 所示:
清單 17. ResultSet 迭代
import groovy.sql.* def sql = Sql.newInstance( "jdbc:derby://localhost:1527/MyDbTest;create=true", "username", "password", "org.apache.derby.jdbc.ClientDriver") println("grab a specific field") sql.eachRow("select name from languages"){ row -> println row.name } println("grab all fields") sql.eachRow("select * from languages"){ row -> println("Name: ${row.name}") println("Version: ${row.version}") println("URL: ${row.url}\n") }
該腳本的第一行代碼實例化了一個新的 Sql 對象:設置 JDBC 連接字符串、用戶名、密碼和 JDBC 驅動器類。這時,可以調用 eachRow() 方法,傳遞 SQL select 語句作為一個方法參數。在閉包內部, 可以引用列名(name、version、url),就好像實際存在 getName()、getVersion() 和 getUrl() 方法 一樣。
這顯然要比 Java 語言中的等效方法更加清晰。在 Java 中,必須創建單獨的 DriverManager、 Connection、Statement 和 JDBCResultSet,然後必須在嵌套的 try/catch/finally 塊中將它們全部清 除。
對於 Sql 對象,您會認為 each() 或 eachRow() 都是一個合理的方法名。但是在接下來的示例中, 我想您會認為 each() 這個名稱並不能充分表達它的功能。
文件迭代
我從未想過使用原始的 Java 代碼逐行遍歷 java.io.File。當我完成了所有的嵌套的 BufferedReader 和 FileReader 後(更別提每個流程末尾的所有異常處理),我已經忘記最初的目的是 什麼。
清單 18 展示了使用 Java 語言完成的整個過程:
清單 18. Java 文件迭代
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class WalkFile { public static void main(String[] args) { BufferedReader br = null; try { br = new BufferedReader(new FileReader("languages.txt")); String line = null; while((line = br.readLine()) != null) { System.out.println("I know " + line); } } catch(FileNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } finally { if(br != null) { try { br.close(); } catch(IOException e) { e.printStackTrace(); } } } } }
清單 19 展示了 Groovy 中的等效過程:
清單 19. Groovy 文件迭代
def f = new File("languages.txt") f.eachLine{language-> println "I know ${language}" }
這正是 Groovy 的簡潔性真正擅長的方面。現在,我希望您了解為什麼我將 Groovy 稱為 “Java 程 序員的 DSL”。
注意,我在 Groovy 和 Java 語言中同時處理同一個 java.io.File 類。如果該文件不存在,那麼 Groovy 代碼將拋出和 Java 代碼相同的 FileNotFoundException 異常。區別在於,Groovy 沒有已檢測 的異常。在 try/catch/finally 塊中封裝 eachLine() 結構是我自己的愛好 — 而不是一項語言需求。 對於一個簡單的命令行腳本中,我欣賞 清單 19 中的代碼的簡潔性。如果我在運行應用服務的同時執行 相同的迭代,我不能對這些異常坐視不管。我將在與 Java 版本相同的 try/catch 塊中封裝 eachLine () 塊。
File 類對 each() 方法進行了一些修改。其中之一就是 splitEachLine(String separator, Closure closure)。這意味著您不僅可以逐行遍歷文件,同時還可以將它分為不同的標記。清單 20 展 示了一個例子:
清單 20. 分解文件的每一行
// languages.txt // notice the space between the language and the version Java 1.5 Groovy 1.6 JavaScript 1.x // splitTest.groovy def f = new File("languages.txt") f.splitEachLine(" "){words-> words.each{ println it } } // output Java 1.5 Groovy 1.6 JavaScript 1.x
如果處理的是二進制文件,Groovy 還提供了一個 eachByte() 方法。
當然,Java 語言中的 File 並不總是一個文件 — 有時是一個目錄。Groovy 還提供了一些 each() 修改以處理子目錄。
目錄迭代
使用 Groovy 代替 shell 腳本(或批處理腳本)非常容易,因為您能夠方便地訪問文件系統。要獲 得當前目錄的目錄列表,參見清單 21:
清單 21. 目錄迭代
def dir = new File(".")
dir.eachFile{file->
println file
}
eachFile() 方法同時返回了文件和子目錄。使用 Java 語言的 isFile() 和 isDirectory() 方法, 可以完成更復雜的事情。清單 22 展示了一個例子:
清單 22. 分離文件和目錄
def dir = new File(".")
dir.eachFile{file->
if(file.isFile()) {
println "FILE: ${file}"
}else if(file.isDirectory()){
println "DIR: ${file}"
}else{
println "Uh, I'm not sure what it is..."
}
}
由於兩種 Java 方法都返回 boolean 值,可以在代碼中添加一個 Java 三元操作符。清單 23 展示 了一個例子:
清單 23. 三元操作符
def dir = new File(".")
dir.eachFile{file->
println file.isDirectory() ? "DIR: ${file}" : "FILE: ${file}"
}
如果只對目錄有興趣,那麼可以使用 eachDir() 而不是 eachFile()。還提供了 eachDirMatch() 和 eachDirRecurse() 方法。
可以看到,對 File 僅使用 each() 方法並不能提供足夠的含義。典型 each() 方法的語義保存在 File 中,但是方法名更具有描述性,從而提供更多有關這個高級功能的信息。
URL 迭代
理解了如何遍歷 File 後,可以使用相同的原則遍歷 HTTP 請求的響應。Groovy 為 java.net.URL 提供了一個方便的(和熟悉的)eachLine() 方法。
例如,清單 24 將逐行遍歷 ibm.com 主頁的 HTML:
清單 24. URL 迭代
def url = new URL("http://www.ibm.com")
url.eachLine{line->
println line
}
當然,如果這就是您的目的的話,Groovy 提供了一個只包含一行代碼的解決辦法,這主要歸功於 toURL() 方法,它被添加到所有 Strings:"http://www.ibm.com".toURL().eachLine{ println it }。
但是,如果希望對 HTTP 響應執行一些更有用的操作,該怎麼辦呢?具體來講,如果發出的請求指向 一個 RESTful Web 服務,而該服務包含您要解析的 XML,該怎麼做呢?each() 方法將在這種情況下提 供幫助。
XML 迭代
您已經了解了如何對文件和 URL 使用 eachLine() 方法。XML 給出了一個稍微有些不同的問題 — 與逐行遍歷 XML 文檔相比,您可能更希望對逐個元素進行遍歷。
例如,假設您的語言列表存儲在名為 languages.xml 的文件中,如清單 25 所示:
清單 25. languages.xml 文件
<langs>
<language>Java</language>
<language>Groovy</language>
<language>JavaScript</language>
</langs>
Groovy 提供了一個 each() 方法,但是需要做一些修改。如果使用名為 XmlSlurper 的原生 Groovy 類解析 XML,那麼可以使用 each() 遍歷元素。參見清單 26 所示的例子:
清單 26. XML 迭代
def langs = new XmlSlurper().parse("languages.xml")
langs.language.each {
println it
}
//output
Java
Groovy
JavaScript
langs.language.each 語句從名為 <language> 的 <langs> 提取所有元素。如果同時 擁有 <format> 和 <server> 元素,它們將不會出現在 each() 方法的輸出中。
如果覺得這還不夠的話,那麼假設這個 XML 是通過一個 RESTful Web 服務的形式獲得,而不是文件 系統中的文件。使用一個 URL 替換文件的路徑,其余代碼仍然保持不變,如清單 27 所示:
清單 27. Web 服務調用的 XML 迭代
def langs = new XmlSlurper().parse("http://somewhere.com/languages")
langs.language.each{
println it
}
這真是個好方法,each() 方法在這裡用得很好,不是嗎?
結束語
在使用 each() 方法的整個過程中,最妙的部分在於它只需要很少的工作就可以處理大量 Groovy 內 容。解了 each() 方法之後,Groovy 中的迭代就易如反掌了。正如 Raymond 所說,這正是關鍵所在。 一旦了解了如何遍歷 List,那麼很快就會掌握如何遍歷數組、Map、String、Range、enum、SQL ResultSet、File、目錄和 URL,甚至是 XML 文檔的元素。
本文的最後一個示例簡單提到使用 XmlSlurper 實現 XML 解析。在下一期文章中,我將繼續討論這 個問題,並展示使用 Groovy 進行 XML 解析有多麼簡單!您將看到 XmlParser 和 XmlSlurper 的實際 使用,並更好地了解 Groovy 為什麼提供兩個類似但又略有不同的類實現 XML 解析。到那時,希望您能 發現 Groovy 的更多實際應用。
本文配套源碼