實(shí)戰(zhàn) Groovy: for each 剖析(轉(zhuǎn)載)
迭代是編程的基礎(chǔ)。您經(jīng)常會(huì)遇到需要進(jìn)行逐項(xiàng)遍歷的內(nèi)容,比如 List
、File
和 JDBC ResultSet
。Java 語(yǔ)言幾乎總是提供了某種方法幫助您逐項(xiàng)遍歷所需的內(nèi)容,但令人沮喪的是,它并沒有給出一種標(biāo)準(zhǔn)方法。Groovy 的迭代方法非常實(shí)用,在這一點(diǎn)上,Groovy 編程與 Java 編程截然不同。通過一些代碼示例,本文將介紹 Groovy 的萬(wàn)能的 each()
方法,從而將 Java 語(yǔ)言的那些迭代怪癖拋在腦后。
假設(shè)您有一個(gè) Java 編程語(yǔ)言的 java.util.List
。清單 1 展示了在 Java 語(yǔ)言中如何使用編程實(shí)現(xiàn)迭代:
清單 1. Java 列表迭代
import java.util.*; |
由于提供了大部分集合類都可以共享的 java.lang.Iterable
接口,您可以使用相同的方法遍歷 java.util.Set
或 java.util.Queue
。
關(guān)于本系列
Groovy 是一款運(yùn)行在 Java 平臺(tái)之上的現(xiàn)代編程語(yǔ)言。它能夠與現(xiàn)有 Java 代碼無(wú)縫集成,同時(shí)引入了閉包和元編程等出色的新特性。簡(jiǎn)而言之,Groovy 類似于 21 世紀(jì)的 Java 語(yǔ)言。
如果要將新工具集成到開發(fā)工具箱中,最關(guān)鍵的是理解什么時(shí)候需要使用它以及什么時(shí)候不適合使用它。Groovy 可以變得非常強(qiáng)大,但前提是它被適當(dāng)?shù)貞?yīng)用到合適的場(chǎng)景中。因此, 實(shí)戰(zhàn) Groovy 系列旨在展示 Groovy 的實(shí)際使用,以及何時(shí)和如何成功應(yīng)用它。
現(xiàn)在,假設(shè)該語(yǔ)言存儲(chǔ)在 java.util.Map
中。在編譯時(shí),嘗試對(duì) Map
獲取 Iterator
會(huì)導(dǎo)致失敗 — Map
并沒有實(shí)現(xiàn) Iterable
接口。幸運(yùn)的是,可以調(diào)用 map.keySet()
返回一個(gè) Set
,然后就可以繼續(xù)處理。這些小差異可能會(huì)影響您的速度,但不會(huì)妨礙您的前進(jìn)。需要注意的是,List
、Set
和 Queue
實(shí)現(xiàn)了 Iterable
,但是 Map
沒有 — 即使它們位于相同的 java.util
包中。
現(xiàn)在假設(shè)該語(yǔ)言存在于 String
數(shù)組中。數(shù)組是一種數(shù)據(jù)結(jié)構(gòu),而不是類。不能對(duì) String
數(shù)組調(diào)用 .iterator()
,因此必須使用稍微不同的迭代策略。您再一次受到阻礙,但可以使用如清單 2 所示的方法解決問題:
清單 2. Java 數(shù)組迭代
public class ArrayTest{ |
但是等一下 — 使用 Java 5 引入的 for-each 語(yǔ)法怎么樣?它可以處理任何實(shí)現(xiàn) Iterable
的類和數(shù)組,如清單 3 所示:
清單 3. Java 語(yǔ)言的 for-each 迭代
import java.util.*; |
因此,您可以使用相同的方法遍歷數(shù)組和集合(Map
除外)。但是如果語(yǔ)言存儲(chǔ)在 java.io.File
,那該怎么辦?如果存儲(chǔ)在 JDBC ResultSet
,或者存儲(chǔ)在 XML 文檔、java.util.StringTokenizer
中呢?面對(duì)每一種情況,必須使用一種稍有不同的迭代策略。這樣做并不是有什么特殊目的 — 而是因?yàn)椴煌?API 是由不同的開發(fā)人員在不同的時(shí)期開發(fā)的 — 但事實(shí)是,您必須了解 6 個(gè) Java 迭代策略,特別是使用這些策略的特殊情況。
Eric S. Raymond 在他的 The Art of Unix Programming 一書中解釋了 “最少意外原則”。他寫道,“要設(shè)計(jì)可用的接口,最好不要設(shè)計(jì)全新的接口模型。新鮮的東西總是難以入門;會(huì)為用戶帶來(lái)學(xué)習(xí)的負(fù)擔(dān),因此應(yīng)當(dāng)盡量減少新內(nèi) 容。”Groovy 對(duì)迭代的態(tài)度正是采納了 Raymond 的觀點(diǎn)。在 Groovy 中遍歷幾乎任何結(jié)構(gòu)時(shí),您只需要使用 each()
這一種方法。
首先,我將 清單 3 中的 List
重構(gòu)為 Groovy。在這里,只需要直接對(duì)列表調(diào)用 each()
方法并傳遞一個(gè)閉包,而不是將 List
轉(zhuǎn)換成 for
循環(huán)(順便提一句,這樣做并不是特別具有面向?qū)ο蟮奶卣鳎皇菃幔?
創(chuàng)建一個(gè)名為 listTest.groovy 的文件并添加清單 4 中的代碼:
清單 4. Groovy 列表迭代
def list = ["Java", "Groovy", "JavaScript"] list.each{language-> println language } |
清單 4 中的第一行是 Groovy 用于構(gòu)建 java.util.ArrayList
的便捷語(yǔ)法。可以將 println list.class
添加到此腳本來(lái)驗(yàn)證這一點(diǎn)。接下來(lái),只需對(duì)列表調(diào)用 each()
,并在閉包體內(nèi)輸出 language
變量。在閉包的開始處使用 language->
語(yǔ)句命名 language
變量。如果沒有提供變量名,Groovy 提供了一個(gè)默認(rèn)名稱 it
。在命令行提示符中輸入 groovy listTest
運(yùn)行 listTest.groovy。
清單 5 是經(jīng)過簡(jiǎn)化的 清單 4 代碼版本:
清單 5. 使用 Groovy 的
it
變量的迭代// shorter, using the default it variable |
Groovy 允許您對(duì)數(shù)組和 List
交替使用 each()
方法。為了將 ArrayList
改為 String
數(shù)組,必須將 as String[]
添加到行末,如清單 6 所示:
清單 6. Groovy 數(shù)組迭代
def list = ["Java", "Groovy", "JavaScript"] as String[] list.each{println it} |
在 Groovy 中普遍使用 each()
方法,并且 getter 語(yǔ)法非常便捷(getClass()
和 class
是相同的調(diào)用),這使您能夠編寫既簡(jiǎn)潔又富有表達(dá)性的代碼。例如,假設(shè)您希望利用反射顯示給定類的所有公共方法。清單 7 展示了這個(gè)例子:
清單 7. Groovy 反射
def s = "Hello World" |
腳本的最后一行調(diào)用 getClass()
方法。java.lang.Class
提供了一個(gè) getMethods()
方法,后者返回一個(gè)數(shù)組。通過將這些操作串連起來(lái)并對(duì) Method
的結(jié)果數(shù)組調(diào)用 each()
,您只使用了一行代碼就完成了大量工作。
但是,與 Java for-each 語(yǔ)句不同的是,萬(wàn)能的 each()
方法并不僅限于 List
和數(shù)組。在 Java 語(yǔ)言中,故事到此結(jié)束。然而,在 Groovy 中,故事才剛剛開始。
從前文可以看到,在 Java 語(yǔ)言中,無(wú)法直接迭代 Map
。在 Groovy 中,這完全不是問題,如清單 8 所示:
清單 8. Groovy map 迭代
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] map.each{ println it } |
要處理名稱/值對(duì),可以使用隱式的 getKey()
和 getValue()
方法,或在包的開頭部分顯式地命名變量,如清單 9 所示:
清單 9. 從 map 獲得鍵和值
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] |
可以看到,迭代 Map
和迭代其它任何集合一樣自然。
在繼續(xù)研究下一個(gè)迭代例子前,應(yīng)當(dāng)了解 Groovy 中有關(guān) Map
的另一個(gè)語(yǔ)法。與在 Java 語(yǔ)言中調(diào)用 map.get("Java")
不一樣,可以簡(jiǎn)化對(duì) map.Java
的調(diào)用,如清單 10 所示:
清單 10. 獲得 map 值
def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"] |
不可否認(rèn),Groovy 針對(duì) Map
的這種便捷語(yǔ)法非常酷,但這也是在對(duì) Map
使用反射時(shí)引起一些常見問題的原因。對(duì) list.class
的調(diào)用將生成 java.util.ArrayList
,而調(diào)用 map.class
返回 null
。這是因?yàn)楂@得 map 元素的便捷方法覆蓋了實(shí)際的 getter 調(diào)用。Map
中的元素都不具有 class
鍵,因此調(diào)用實(shí)際會(huì)返回 null
,如清單 11 的示例所示:
清單 11. Groovy map 和
null
def list = ["Java", "Groovy", "JavaScript"] |
這是 Groovy 比較罕見的打破 “最少意外原則” 的情況,但是由于從 map 獲取元素要比使用反射更加常見,因此我可以接受這一例外。
現(xiàn)在您已經(jīng)熟悉 each()
方法了,它可以出現(xiàn)在所有相關(guān)的位置。假設(shè)您希望迭代一個(gè) String
,并且是逐一迭代字符,那么馬上可以使用 each()
方法。如清單 12 所示:
清單 12.
String
迭代def name = "Jane Smith" name.each{letter-> |
這提供了所有的可能性,比如使用下劃線替代所有空格,如清單 13 所示:
清單 13. 使用下劃線替代空格
|
當(dāng)然,在替換一個(gè)單個(gè)字母時(shí),Groovy 提供了一個(gè)更加簡(jiǎn)潔的替換方法。您可以將清單 13 中的所有代碼合并為一行代碼:"Jane Smith".replace(" ", "_")
。但是對(duì)于更復(fù)雜的 String
操作,each()
方法是最佳選擇。
Groovy 提供了原生的 Range
類型,可以直接迭代。使用兩個(gè)點(diǎn)分隔的所有內(nèi)容(比如 1..10
)都是一個(gè) Range
。清單 14 展示了這個(gè)例子:
清單 14. Range 迭代
def range = 5..10 range.each{ |
Range
不局限于簡(jiǎn)單的 Integer
。考慮清單 15 在的代碼,其中迭代 Date
的 Range
:
清單 15.
Date
迭代def today = new Date() |
可以看到,each()
準(zhǔn)確地出現(xiàn)在您所期望的位置。Java 語(yǔ)言缺乏原生的 Range
類型,但是提供了一個(gè)類似地概念,采取 enum
的形式。毫不奇怪,在這里 each()
仍然派得上用場(chǎng)。
Java enum
是按照特定順序保存的隨意的值集合。清單 16 展示了 each()
方法如何自然地配合 enum
,就好象它在處理 Range
操作符一樣:
清單 16.
enum
迭代enum DAY{ |
在 Groovy 中,有些情況下,each()
這個(gè)名稱遠(yuǎn)未能表達(dá)它的強(qiáng)大功能。在下面的例子中,將看到使用特定于所用上下文的方法對(duì) each()
方法進(jìn)行修飾。Groovy eachRow()
方法就是一個(gè)很好的例子。
在處理關(guān)系數(shù)據(jù)庫(kù)表時(shí),經(jīng)常會(huì)說(shuō) “我需要針對(duì)表中的每一行執(zhí)行操作”。比較一下前面的例子。您很可能會(huì)說(shuō) “我需要對(duì)列表中的每一種語(yǔ)言執(zhí)行一些操作”。根據(jù)這個(gè)道理,groovy.sql.Sql
對(duì)象提供了一個(gè) eachRow()
方法,如清單 17 所示:
清單 17.
ResultSet
迭代import groovy.sql.* |
該腳本的第一行代碼實(shí)例化了一個(gè)新的 Sql
對(duì)象:設(shè)置 JDBC 連接字符串、用戶名、密碼和 JDBC 驅(qū)動(dòng)器類。這時(shí),可以調(diào)用 eachRow()
方法,傳遞 SQL select
語(yǔ)句作為一個(gè)方法參數(shù)。在閉包內(nèi)部,可以引用列名(name
、version
、url
),就好像實(shí)際存在 getName()
、getVersion()
和 getUrl()
方法一樣。
這顯然要比 Java 語(yǔ)言中的等效方法更加清晰。在 Java 中,必須創(chuàng)建單獨(dú)的 DriverManager
、Connection
、Statement
和 JDBCResultSet
,然后必須在嵌套的 try
/catch
/finally
塊中將它們?nèi)壳宄?
對(duì)于 Sql
對(duì)象,您會(huì)認(rèn)為 each()
或 eachRow()
都是一個(gè)合理的方法名。但是在接下來(lái)的示例中,我想您會(huì)認(rèn)為 each()
這個(gè)名稱并不能充分表達(dá)它的功能。
我從未想過使用原始的 Java 代碼逐行遍歷 java.io.File
。當(dāng)我完成了所有的嵌套的 BufferedReader
和 FileReader
后(更別提每個(gè)流程末尾的所有異常處理),我已經(jīng)忘記最初的目的是什么。
清單 18 展示了使用 Java 語(yǔ)言完成的整個(gè)過程:
清單 18. Java 文件迭代
import java.io.BufferedReader; |
清單 19 展示了 Groovy 中的等效過程:
清單 19. Groovy 文件迭代
def f = new File("languages.txt") f.eachLine{language-> |
這正是 Groovy 的簡(jiǎn)潔性真正擅長(zhǎng)的方面。現(xiàn)在,我希望您了解為什么我將 Groovy 稱為 “Java 程序員的 DSL”。
注意,我在 Groovy 和 Java 語(yǔ)言中同時(shí)處理同一個(gè) java.io.File
類。如果該文件不存在,那么 Groovy 代碼將拋出和 Java 代碼相同的 FileNotFoundException
異常。區(qū)別在于,Groovy 沒有已檢測(cè)的異常。在 try
/catch
/finally
塊中封裝 eachLine()
結(jié)構(gòu)是我自己的愛好 — 而不是一項(xiàng)語(yǔ)言需求。對(duì)于一個(gè)簡(jiǎn)單的命令行腳本中,我欣賞 清單 19 中的代碼的簡(jiǎn)潔性。如果我在運(yùn)行應(yīng)用服務(wù)的同時(shí)執(zhí)行相同的迭代,我不能對(duì)這些異常坐視不管。我將在與 Java 版本相同的 try/catch
塊中封裝 eachLine()
塊。
File
類對(duì) each()
方法進(jìn)行了一些修改。其中之一就是 splitEachLine(String separator, Closure closure)
。這意味著您不僅可以逐行遍歷文件,同時(shí)還可以將它分為不同的標(biāo)記。清單 20 展示了一個(gè)例子:
清單 20. 分解文件的每一行
// languages.txt |
如果處理的是二進(jìn)制文件,Groovy 還提供了一個(gè) eachByte()
方法。
當(dāng)然,Java 語(yǔ)言中的 File
并不總是一個(gè)文件 — 有時(shí)是一個(gè)目錄。Groovy 還提供了一些 each()
修改以處理子目錄。
使用 Groovy 代替 shell 腳本(或批處理腳本)非常容易,因?yàn)槟軌蚍奖愕卦L問文件系統(tǒng)。要獲得當(dāng)前目錄的目錄列表,參見清單 21:
清單 21. 目錄迭代
def dir = new File(".") dir.eachFile{file-> |
eachFile()
方法同時(shí)返回了文件和子目錄。使用 Java 語(yǔ)言的 isFile()
和 isDirectory()
方法,可以完成更復(fù)雜的事情。清單 22 展示了一個(gè)例子:
清單 22. 分離文件和目錄
def dir = new File(".") dir.eachFile{file-> |
由于兩種 Java 方法都返回 boolean
值,可以在代碼中添加一個(gè) Java 三元操作符。清單 23 展示了一個(gè)例子:
清單 23. 三元操作符
def dir = new File(".") |
如果只對(duì)目錄有興趣,那么可以使用 eachDir()
而不是 eachFile()
。還提供了 eachDirMatch()
和 eachDirRecurse()
方法。
可以看到,對(duì) File
僅使用 each()
方法并不能提供足夠的含義。典型 each()
方法的語(yǔ)義保存在 File
中,但是方法名更具有描述性,從而提供更多有關(guān)這個(gè)高級(jí)功能的信息。
理解了如何遍歷 File
后,可以使用相同的原則遍歷 HTTP 請(qǐng)求的響應(yīng)。Groovy 為 java.net.URL
提供了一個(gè)方便的(和熟悉的)eachLine()
方法。
例如,清單 24 將逐行遍歷 ibm.com 主頁(yè)的 HTML:
清單 24. URL 迭代
def url = new URL("http://www.ibm.com") |
當(dāng)然,如果這就是您的目的的話,Groovy 提供了一個(gè)只包含一行代碼的解決辦法,這主要?dú)w功于 toURL()
方法,它被添加到所有 Strings
:"http://www.ibm.com".toURL().eachLine{ println it }
。
但是,如果希望對(duì) HTTP 響應(yīng)執(zhí)行一些更有用的操作,該怎么辦呢?具體來(lái)講,如果發(fā)出的請(qǐng)求指向一個(gè) RESTful Web 服務(wù),而該服務(wù)包含您要解析的 XML,該怎么做呢?each()
方法將在這種情況下提供幫助。
您已經(jīng)了解了如何對(duì)文件和 URL 使用 eachLine()
方法。XML 給出了一個(gè)稍微有些不同的問題 — 與逐行遍歷 XML 文檔相比,您可能更希望對(duì)逐個(gè)元素進(jìn)行遍歷。
例如,假設(shè)您的語(yǔ)言列表存儲(chǔ)在名為 languages.xml 的文件中,如清單 25 所示:
清單 25. languages.xml 文件
<langs> |
Groovy 提供了一個(gè) each()
方法,但是需要做一些修改。如果使用名為 XmlSlurper
的原生 Groovy 類解析 XML,那么可以使用 each()
遍歷元素。參見清單 26 所示的例子:
清單 26. XML 迭代
def langs = new XmlSlurper().parse("languages.xml") |
langs.language.each
語(yǔ)句從名為 <language>
的 <langs>
提取所有元素。如果同時(shí)擁有 <format>
和 <server>
元素,它們將不會(huì)出現(xiàn)在 each()
方法的輸出中。
如果覺得這還不夠的話,那么假設(shè)這個(gè) XML 是通過一個(gè) RESTful Web 服務(wù)的形式獲得,而不是文件系統(tǒng)中的文件。使用一個(gè) URL 替換文件的路徑,其余代碼仍然保持不變,如清單 27 所示:
清單 27. Web 服務(wù)調(diào)用的 XML 迭代
def langs = new XmlSlurper().parse("http://somewhere.com/languages") |
這真是個(gè)好方法,each()
方法在這里用得很好,不是嗎?
在使用 each()
方法的整個(gè)過程中,最妙的部分在于它只需要很少的工作就可以處理大量 Groovy 內(nèi)容。解了 each()
方法之后,Groovy 中的迭代就易如反掌了。正如 Raymond 所說(shuō),這正是關(guān)鍵所在。一旦了解了如何遍歷 List
,那么很快就會(huì)掌握如何遍歷數(shù)組、Map
、String
、Range
、enum
、SQL ResultSet
、File
、目錄和 URL
,甚至是 XML 文檔的元素。
本文的最后一個(gè)示例簡(jiǎn)單提到使用 XmlSlurper
實(shí)現(xiàn) XML 解析。在下一期文章中,我將繼續(xù)討論這個(gè)問題,并展示使用 Groovy 進(jìn)行 XML 解析有多么簡(jiǎn)單!您將看到 XmlParser
和 XmlSlurper
的實(shí)際使用,并更好地了解 Groovy 為什么提供兩個(gè)類似但又略有不同的類實(shí)現(xiàn) XML 解析。到那時(shí),希望您能發(fā)現(xiàn) Groovy 的更多實(shí)際應(yīng)用。
posted on 2011-08-04 11:50 小羅 閱讀(553) 評(píng)論(0) 編輯 收藏 所屬分類: groovy