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

Groovy 開發(fā)工具包

1. 處理 IO 操作

Groovy 提供了很多輔助方法,盡管可以使用 Java 來解決,但 Groovy 還是更多方便的方法來處理文件、流、閱讀器,等等。

你特別需要注意添加到這些類中的方法:

下面,使用上面提供的輔助函數(shù)來介紹一些簡單而又符合 Groovy 語言習(xí)慣的構(gòu)建范例,更多的有關(guān)方法可參看 GDK API。

1.1 讀取文件

作為第一個范例,我們來看一看如何將文本文件的所有行都打印出來:

new File(baseDir, 'haiku.txt').eachLine { line ->
    println line
}  

eachLine 方法是由 Groovy 自動添加到 File 文件上的方法。它可以有多個變體,假如你需要知道行號,可以使用下面這個變體:

new File(baseDir, 'haiku.txt').eachLine { line, nb ->
    println "Line $nb: $line"
}  

假如因為某種原因,eachLine 語句體內(nèi)拋出了異常,方法需要確定資源是否正確關(guān)閉。這一點對于所有 Groovy 添加的 I/O 資源方法來說都是適用的。

比如在一些情況下,你更喜歡用 Reader,但仍然會受益于 Groovy 的自動資源管理。在下面這個例子中,即使發(fā)生異常,Reader 也會關(guān)閉。

def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
    while (reader.readLine()) {
        if (++count > MAXSIZE) {
            throw new RuntimeException('Haiku should only have 3 verses')
        }
    }
}

有時,你可能會需要將某個文本文件的行放入列表中,可以這樣寫:

def list = new File(baseDir, 'haiku.txt').collect {it}

或者,你甚至還可以利用 as 操作符將獲得的文件內(nèi)容放到一個關(guān)于行的數(shù)組中:

def array = new File(baseDir, 'haiku.txt') as String[]

你還記得有多少次必須將文件內(nèi)容用 byte[] 放到一個字節(jié)數(shù)組中而要寫多少代碼嗎?現(xiàn)在,Groovy 將一切都簡化了:

byte[] contents = file.bytes

處理 I/O 并不限于操作文件。實際上,很多操作都依賴于輸入輸出流,這就是 Groovy 為何要重新添加大量方法的原因。關(guān)于這些,可以查看詳細(xì)的文檔。

再次舉例,我們可以非常輕松地從一個 File 中獲取 InputStream

def is = new File(baseDir,'haiku.txt').newInputStream()
// 省略的邏輯語句......
is.close()

但是,從上面的代碼我們也可以看到,你還需要手動關(guān)閉這個輸入流的。在 Groovy 中,一般最好使用 withInputStream 習(xí)語,它可以替你處理這個關(guān)閉操作:

new File(baseDir,'haiku.txt').withInputStream { stream ->
    // 省略的邏輯語句......
}

1.2 寫入文件

在某些情況下,你可能只需要寫入文件,而不需要讀取。這時,使用 Writer 是一種不錯的方法:

new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}

但對于這么簡單的一個例子來說,使用 << 無疑就夠用了:

new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''

當(dāng)然我們并不一定總是處理文本內(nèi)容,使用 Writer 還可以直接寫入字節(jié):

file.bytes = [66,22,11]

無疑,你也可以直接處理輸出流,比如下面這個例子就通過創(chuàng)建輸出流,再將其寫入一個文件:

def os = new File(baseDir,'data.bin').newOutputStream()
// 省略的邏輯語句......
os.close()

但我們再次看到,需要手動編寫關(guān)閉輸出流的語句,與輸入流的情況相同,一般在 Groovy 中最好使用 withOutputStream 習(xí)語,因為它既可以自動處理異常,也能在任何情況下關(guān)閉輸出流。

new File(baseDir,'data.bin').withOutputStream { stream ->
    // 省略的邏輯語句......
}

1.3 遍歷文件樹

在編寫上下文腳本時,常見的一個任務(wù)就是遍歷文件樹查找某些特定文件,然后對它們進(jìn)行一定的處理。Groovy 為此提供了多種方法。例如,你可以對某個目錄的所有文件執(zhí)行一些操作:

dir.eachFile { file ->                    // 1??

    println file.name
}
dir.eachFileMatch(~/.*\.txt/) { file ->   // 2??
    println file.name
}    

1?? 對目錄中已找到的所有文件執(zhí)行閉包代碼。
2?? 對匹配特定模式的目錄中的文件執(zhí)行閉包代碼。

另外,我們往往必須處理較深層次的文件,這種情況下可以使用 eachFileRecurse

dir.eachFileRecurse { file ->                  // 1??                   
    println file.name
}

dir.eachFileRecurse(FileType.FILES) { file ->  // 2??
    println file.name
}

1?? 對目錄中已找到的所有文件或目錄遞歸地執(zhí)行閉包代碼。
2?? 只在文件上執(zhí)行閉包代碼,但并不遞歸。

要想使用更復(fù)雜的遍歷技術(shù),你可以使用 traverse 方法,它需要你設(shè)置特定的標(biāo)志來確定遍歷的行為:

dir.traverse { file ->
    if (file.directory && file.name=='bin') {
        FileVisitResult.TERMINATE                // 1??           
    } else {
        println file.name
        FileVisitResult.CONTINUE                 // 2??    
    }

}

1?? 如果當(dāng)前文件是目錄或者它的名稱為 bin,則停止遍歷。
2?? 如果不滿足 1?? 條件,則打印文件名并繼續(xù)遍歷。

1.4 數(shù)據(jù)和對象

在 Java 中,利用 java.io.DataOutputStreamjava.io.DataInputStream 來序列化與反序列化數(shù)據(jù)的情況并不少見。Groovy 使得(反)序列化處理更容易了。比如可以利用下面這些代碼來將數(shù)據(jù)序列化到文件中,然后再將文件反序列化:

boolean b = true
String message = 'Hello from Groovy'
// 將文件序列化到文件中
file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
}
// ...
// 然后再重新把它讀取出來
file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
}  

同樣,如果想要序列化的數(shù)據(jù)實現(xiàn)了 Serializable 接口,你可以像下面這樣利用一個對象輸出流:

Person p = new Person(name:'Bob', age:76)
// 將文件序列化到文件中
file.withObjectOutputStream { out ->
    out.writeObject(p)
}
// ...
// 然后再重新把它讀取出來
file.withObjectInputStream { input ->
    def p2 = input.readObject()
    assert p2.name == p.name
    assert p2.age == p.age
}

1.5 執(zhí)行外部進(jìn)程

前面幾節(jié)介紹了在 Groovy 中處理文件、Reader 或流的便利性,但在系統(tǒng)管理或開發(fā)運維領(lǐng)域中,我們經(jīng)常還需要與外部進(jìn)程進(jìn)行通信。

Groovy 執(zhí)行命令行進(jìn)程的方式非常簡單,只需把命令行寫成字符串的形式,然后調(diào)用 execute() 方法即可。比如,在 nix 系統(tǒng)的機(jī)器上(或者是 Windows 機(jī)器上安裝了合適的 nix 命令),可以執(zhí)行類似下面的命令:

def process = "ls -l".execute()          // 1??          
println "Found text ${process.text}"     // 2??       

1?? 在外部進(jìn)程中執(zhí)行 ls 命令。
2?? 利用命令輸出獲取文本。

execute() 方法返回一個 java.lang.Process 實例,該實例隨后能允許執(zhí)行 in/out/err 流,檢查處理得到的結(jié)束值,等等。

比如,下例中的命令與上例相同,但這次我們每次只處理一行結(jié)果流:

def process = "ls -l".execute()  // 1??           
process.in.eachLine { line ->    // 2??         
    println line                 // 3??      
}  

1?? 在外部進(jìn)程中執(zhí)行 ls 命令。
2?? 對于進(jìn)程的輸入流的每一行......
3?? 打印改行。

上例中,值得我們注意的是,in 對應(yīng)著一個輸入流標(biāo)準(zhǔn)輸出。out 則引用一個將數(shù)據(jù)發(fā)送處理(它的標(biāo)準(zhǔn)輸入)的流。

另外一點要記住的是,很多命令都是 shell 內(nèi)置的,需要特殊處理。假設(shè)在 Windows 系統(tǒng)機(jī)器上想用以下方式獲取文件列表:

def process = "dir".execute()  
println "${process.text}"  

你會得到這樣一個 IOException 異常:Cannot run program "dir": CreateProcess error=2, The system cannot find the file specified.(無法運行程序 “dir”:CreateProcess error=2,系統(tǒng)無法找到指定目錄。)

這是因為 dir 是 Windows shell(cmd.exe)的內(nèi)建命令,無法以一個簡單的可執(zhí)行文件的形式運行。你需要這樣寫:

def process = "cmd /c dir".execute()  
println "${process.text}"  

另外,由于該功能當(dāng)前其實秘密使用了 java.lang.Process 這個類,所以就必須要提防該類的一些不足和缺點。特別要注意的是,在 java 文檔中對該類有如下這番說明:

> 由于一些原生平臺上所提供的輸出和輸入流的緩沖區(qū)十分有限,所以如果未能及時地寫入子進(jìn)程的輸入流或讀取子進(jìn)程的輸出流,可能會導(dǎo)致子進(jìn)程的阻塞甚至死鎖。

正是由于這個特點,Groovy 提供了一些額外的幫助方法來使進(jìn)程流的控制更為容易。

下面這個例子說明了如何獲取進(jìn)程中的所有輸出(包括錯誤流輸出):

def p = "rm -f foo.tmp".execute([], tmpDir)  
p.consumeProcessOutput()  
p.waitFor()  

目前存在很多 consumeProcessOutput 的變體形式,它們可以使用 StringBuffer、InputStreamOutputStream,等等。要想獲取完整的信息,請參考 java.lang.Process 的 GDK API。

另外,pipeTo 命令(映射到 | 以便允許過載)可以把某一個進(jìn)程的輸出流提供給另一個進(jìn)程的輸入流。

下面介紹一些有關(guān)它的用例:

管道實例:

proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
    println proc4.err.text
} else {
    println proc4.text
}

嚴(yán)重的錯誤用法:

def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
    writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"

2. 使用集合

Groovy 為多種集合提供了原生支持,這些類型的集合包括:list、map(映射) 或 range(范圍)。它們大多基于 java 集合類型,并且附帶有 GDK中的一些額外方法。

2.1 List(列表)

2.1.1 列表字面量

你可以按照如下方式創(chuàng)建 lists,注意 [] 是空 list 表達(dá)式。

def list = [5, 6, 7, 8]
assert list.get(2) == 7
assert list[2] == 7
assert list instanceof java.util.List

def emptyList = []
assert emptyList.size() == 0
emptyList.add(5)
assert emptyList.size() == 1

每一個 list 表達(dá)式都創(chuàng)建了一個 java.util.List 接口的實現(xiàn)。

當(dāng)然也可以把 list 作為構(gòu)建其他 list 的來源:

def list1 = ['a', 'b', 'c']
//基于 list1 中的項構(gòu)建一個新的列表
def list2 = new ArrayList<String>(list1)

assert list2 == list1 // == 用來檢查兩個列表中的每個相應(yīng)元素是否相同

// 也可以調(diào)用 clone()   
def list3 = list1.clone()
assert list3 == list1    

列表其實是一個有序的對象集合:

def list = [5, 6, 7, 8]
assert list.size() == 4
assert list.getClass() == ArrayList     // 特定類型的列表

assert list[2] == 7                     // 起始索引為 0
assert list.getAt(2) == 7               // 下標(biāo)操作符 [] 的等效方法
assert list.get(2) == 7                 // 另一種方法

list[2] = 9
assert list == [5, 6, 9, 8,]           // 允許末尾出現(xiàn)逗號

list.putAt(2, 10)                       // 當(dāng)數(shù)值發(fā)生變動時,[] 的等效方法
assert list == [5, 6, 10, 8]
assert list.set(2, 11) == 10            // 返回舊值的另一種方法
assert list == [5, 6, 11, 8]

assert ['a', 1, 'a', 'a', 2.5, 2.5f, 2.5d, 'hello', 7g, null, 9 as byte]  
// 對象的類型可以不同,對象也允許重復(fù)。

assert [1, 2, 3, 4, 5][-1] == 5             // 使用負(fù)數(shù)代表從末尾開始計算索引
assert [1, 2, 3, 4, 5][-2] == 4
assert [1, 2, 3, 4, 5].getAt(-2) == 4       // getAt() 允許出現(xiàn)負(fù)數(shù)索引......
try {
    [1, 2, 3, 4, 5].get(-2)                 // 但負(fù)數(shù)索引不允許用于 get() 方法
    assert false
} catch (e) {
    assert e instanceof ArrayIndexOutOfBoundsException
}

2.1.2 作為布爾表達(dá)式的列表

可以將列表用作 boolean 值:

assert ![]             // 空列表的結(jié)果是 false  

//所有其他列表,不管其中包含的內(nèi)容,其布爾值都是 true  
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]

2.1.3 列表中的迭代

通常,列表中的迭代需要調(diào)用 eacheachWithIndex 方法來實現(xiàn),在列表中的每一項上都將執(zhí)行一定的代碼:

[1, 2, 3].each {
    println "Item: $it" // `it` 是個很隱含的參數(shù),對應(yīng)著當(dāng)前元素  
}
['a', 'b', 'c'].eachWithIndex { it, i -> // `it` 是當(dāng)前元素,而 `i` 則是索引   
    println "$i: $it"
}

除了迭代之外,往往還用到將列表中的每個元素轉(zhuǎn)換成其他內(nèi)容,從而創(chuàng)建一個新的列表。這種操作常被稱為映射(mapping),在 Groovy 中利用 collect 方法來實現(xiàn):

assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]

// 代替 `collect` 的快捷格式
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }

def list = [0]
// 有可能要給 `collect` 提供收集元素的列表    
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]

2.1.4 操縱列表

過濾與搜索

Groovy Development Kit中包含著與集合相關(guān)的大量方法,這些實用性的方法大大增強(qiáng)了集合的標(biāo)準(zhǔn)操作。其中一些方法如下所示:

assert [1, 2, 3].find { it > 1 } == 2           // 查找符合規(guī)則的第一個元素
assert [1, 2, 3].findAll { it > 1 } == [2, 3]   // 查找符合規(guī)則的所有元素
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf {      // 查找符合規(guī)則的第一個元素的索引  
    it in ['c', 'e', 'g']
} == 2

assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2  // 返回索引
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // 索引 -1 意指該值并不在列表中
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4

assert [1, 2, 3].every { it < 5 }               // 如果所有元素都符合斷言,則返回 true
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 }                 // 只要有元素符合斷言,就返回 true
assert ![1, 2, 3].any { it > 3 }

assert [1, 2, 3, 4, 5, 6].sum() == 21                // 利用 plus() 方法對任何數(shù)值進(jìn)行加和運算
assert ['a', 'b', 'c', 'd', 'e'].sum {
    it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
    // sum 中使用的自定義值
} == 15
assert ['a', 'b', 'c', 'd', 'e'].sum { ((char) it) - ((char) 'a') } == 10
assert ['a', 'b', 'c', 'd', 'e'].sum() == 'abcde'
assert [['a', 'b'], ['c', 'd']].sum() == ['a', 'b', 'c', 'd']

// 可以提供一個初始化值
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006

assert [1, 2, 3].join('-') == '1-2-3'           // 字符串連接
assert [1, 2, 3].inject('counting: ') {
    str, item -> str + item                     // 減少操作
} == 'counting: 123'
assert [1, 2, 3].inject(0) { count, item ->
    count + item
} == 6

下面是 Groovy 中慣用的一種尋找集合中最大值與最小值的方法:

def list = [9, 4, 2, 10, 5]
assert list.max() == 10
assert list.min() == 2

// 跟任何類似的可對比對象一樣,我們也可以比較單個的字符
assert ['x', 'y', 'a', 'z'].min() == 'a'

// 可以使用閉包來指定排序行為  
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list2.max { it.size() } == 'xyzuvw'
assert list2.min { it.size() } == 'z'   

除了閉包之外,還可以使用 Comparator 來定義比較條件:

Comparator mc = { a, b -> a == b ? 0 : (a < b ? -1 : 1) }

def list = [7, 4, 9, -6, -1, 11, 2, 3, -9, 5, -13]
assert list.max(mc) == 11
assert list.min(mc) == -13

Comparator mc2 = { a, b -> a == b ? 0 : (Math.abs(a) < Math.abs(b)) ? -1 : 1 }

assert list.max(mc2) == -13
assert list.min(mc2) == -1

assert list.max { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -13
assert list.min { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -1
添加或去除元素

我們可以使用 [] 來指定一個新的空列表,然后用 << 將列表項添加進(jìn)去:

def list = []
assert list.empty

list << 5
assert list.size() == 1

list << 7 << 'i' << 11
assert list == [5, 7, 'i', 11]

list << ['m', 'o']
assert list == [5, 7, 'i', 11, ['m', 'o']]

// `<<` 鏈中的第一項是目標(biāo)列表  
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]

// 使用 `leftShift` 等同于使用 `<<`  
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))

我們可以用多種方式為列表添加元素:

assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// 等于調(diào)用 `plus` 方法
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]

def a = [1, 2, 3]
a += 4      // 創(chuàng)建一個新列表,并以這種方式為 `a` 添加新的元素  
a += [5, 6]
assert a == [1, 2, 3, 4, 5, 6]

assert [1, *[222, 333], 456] == [1, 222, 333, 456]
assert [*[1, 2, 3]] == [1, 2, 3]
assert [1, [2, 3, [4, 5], 6], 7, [8, 9]].flatten() == [1, 2, 3, 4, 5, 6, 7, 8, 9]

def list = [1, 2]
list.add(3)
list.addAll([5, 4])
assert list == [1, 2, 3, 5, 4]

list = [1, 2]
list.add(1, 3) // 把 3 添加到索引為 1 的元素之前  
assert list == [1, 3, 2]

list.addAll(2, [5, 4]) //將 [5,4] 添加到索引為 2 的元素之前   
assert list == [1, 3, 5, 4, 2]

list = ['a', 'b', 'z', 'e', 'u', 'v', 'g']
list[8] = 'x' // `[]` 可以當(dāng)操作符使用,從而按需擴(kuò)展列表     
// 如果需要,也可以在列表中添加 null 值   
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']

但要提請大家注意的是,列表中的 + 操作符生成的結(jié)果是不可變的。與 << 操作符相比,使用它會創(chuàng)建一個新列表,而這往往不是你想要的結(jié)果,容易引起性能問題。

Groovy Development Kit 中還包含了一些可以通過值來去除列表元素的方法:

assert ['a','b','c','b','b'] - 'c' == ['a','b','b','b']
assert ['a','b','c','b','b'] - 'b' == ['a','c']
assert ['a','b','c','b','b'] - ['b','c'] == ['a']

def list = [1,2,3,4,3,2,1]
list -= 3           // 從原始列表中去除 `3` 創(chuàng)建一個新列表   
assert list == [1,2,4,2,1]
assert ( list -= [2,4] ) == [1,1]

也可以按照索引來去除列表元素,這時列表會發(fā)生變化:

def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3          // 去除第 3 個元素,然后返回該元素  
assert list == [1,2,4,5,6,2,2,1]

如果你只想去除列表中第一個跟值相同的元素,而不是所有元素的話,可以使用 remove 方法:

def list= ['a','b','c','b','b']
assert list.remove('c')             // 去除 'c' 并返回 true,因為元素已經(jīng)清除了  
assert list.remove('b')             // 去除第一個 'b' 并返回 true,因為元素已經(jīng)清除了

assert ! list.remove('z')           // 返回 false,因為沒有這個元素可供清除  
assert list == ['a','b','b']

去除列表中所有元素,可以使用 clear 方法:

def list= ['a',2,'c',4]
list.clear()
assert list == []
集合操作

Groovy Development Kit 中還包含了一些便于操作集合的方法:

assert 'a' in ['a','b','c']             // 如果某一元素屬于該列表,則返回 true  
assert ['a','b','c'].contains('a')      // 等同于 Java 中的 `contains` 方法   
assert [1,3,4].containsAll([1,4])       // `containsAll` 將檢查是否已經(jīng)找到所有的元素  

assert [1,2,3,3,3,3,4,5].count(3) == 4  // 計算匹配相應(yīng)值的元素的數(shù)目  
assert [1,2,3,3,3,3,4,5].count {
    it%2==0                             // 計算符合謂語要求的元素數(shù)目  
} == 2

assert [1,2,4,6,8,10,12].intersect([1,3,6,9,12]) == [1,6,12]

assert [1,2,3].disjoint( [4,6,9] )
assert ![1,2,3].disjoint( [2,4,6] )
排序

操作集合往往會用到排序。Groovy 提供了多種列表排序方法,從使用閉包到比較器,如下所示:

assert [6, 3, 9, 2, 7, 1, 5].sort() == [1, 2, 3, 5, 6, 7, 9]

def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list.sort {
    it.size()
} == ['z', 'abc', '321', 'Hello', 'xyzuvw']

def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]
assert list2.sort { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } ==
        [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]

Comparator mc = { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 }

// 只適用于 JDK 8+ 
// list2.sort(mc)
// assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]

def list3 = [6, -3, 9, 2, -7, 1, 5]

Collections.sort(list3)
assert list3 == [-7, -3, 1, 2, 5, 6, 9]

Collections.sort(list3, mc)
assert list3 == [1, 2, -3, 5, 6, -7, 9]  
復(fù)制元素

Groovy Development Kit 中利用操作符重載來提供列表元素的復(fù)制方法。

assert [1, 2, 3] * 3 == [1, 2, 3, 1, 2, 3, 1, 2, 3]
assert [1, 2, 3].multiply(2) == [1, 2, 3, 1, 2, 3]
assert Collections.nCopies(3, 'b') == ['b', 'b', 'b']

// JDK 的 nCopies 的語義跟列表所用的 multiply 截然不同
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] // 而不是 [1,2,1,2]

2.2 Maps

2.2.1 Map 字面量

在 Groovy 中,Map(也被稱為關(guān)聯(lián)數(shù)組)可以通過map 的字面格式([:])來創(chuàng)建:

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.get('name') == 'Gromit'
assert map.get('id') == 1234
assert map['name'] == 'Gromit'
assert map['id'] == 1234
assert map instanceof java.util.Map

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.put("foo", 5)
assert emptyMap.size() == 1
assert emptyMap.get("foo") == 5

Map 的鍵默認(rèn)都是字符串:[a:1]['a':1] 是相等的。這個特點有時可能會讓人感到困擾,比如當(dāng)你定義了一個名為 a 的變量,而又想把 a定義為 map 中的鍵時。在這種情況下,你必須用括號把鍵轉(zhuǎn)義,如下所示:

def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null // 沒有找到 `Bob` 
assert ages['a'] == 43     // 因為 `a` 是一個字面量!

ages = [(a): 43]            // 現(xiàn)在用括號把 `a` 轉(zhuǎn)義
assert ages['Bob'] == 43   // 就找到 `Bob` 了!

除了 map 字面量之外,還可以克隆 map,獲得一個新的 map 副本:

def map = [
        simple : 123,
        complex: [a: 1, b: 2]
]
def map2 = map.clone()
assert map2.get('simple') == map.get('simple')
assert map2.get('complex') == map.get('complex')
map2.get('complex').put('c', 3)
assert map.get('complex').get('c') == 3

結(jié)果的 map 是原始 map 的淺層副本,如前例所示。

2.2.2 Map 屬性表示法

Map 也可以像 bean 那樣,只要鍵是字符串這種有效的 Groovy 標(biāo)識符,就可以使用屬性表示法來獲取/設(shè)置 Map 中的元素。

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit'     // 可以替代 map.get('Gromit') 
assert map.id == 1234

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5

注意:map.foo 有意被設(shè)計成一直搜索 map 中的 foo 鍵。這意味著在一個不含有 class 鍵的 map 中,foo.class 將會返回 null。如果你真的想知道類的話,必須使用 getClass() 方法。

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.class == null
assert map.get('class') == null
assert map.getClass() == LinkedHashMap // 這可能是你想要的   

map = [1      : 'a',
       (true) : 'p',
       (false): 'q',
       (null) : 'x',
       'null' : 'z']
assert map.containsKey(1) // 1 不是標(biāo)識符,所以可以這樣用   
assert map.true == null
assert map.false == null
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z'
assert map.get(null) == 'x'

2.2.3 對 map 的迭代

Groovy Development Kit,對 map 慣用的迭代會使用 eacheachWithIndex 兩種方法。值得注意的是,使用 map 字面量標(biāo)記定義的 map 都是有序的。也就是說,如果你迭代 map 中的元素,元素項絕對會按照它們添加進(jìn) map 的順序返回。

def map = [
        Bob  : 42,
        Alice: 54,
        Max  : 33
]

// `entry` 是一個 map 項
map.each { entry ->
    println "Name: $entry.key Age: $entry.value"
}

// `entry` 是一個 map 項,`i` 是 map 中的索引
map.eachWithIndex { entry, i ->
    println "$i - Name: $entry.key Age: $entry.value"
}

// 當(dāng)然你還可以直接利用鍵與值來進(jìn)行迭代  
map.each { key, value ->
    println "Name: $key Age: $value"
}

// Key、value 和 i 都可以作為 map 中的索引  
map.eachWithIndex { key, value, i ->
    println "$i - Name: $key Age: $value"
}

2.2.4 操縱 map

添加或去除元素

為 map 添加元素,可以使用 put 方法,下標(biāo)運算符,或使用 putAll

def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']

def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides)
assert result == [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']

去除 map 中所有元素,可以使用 clear 方法:

def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]

使用 map 字面量格式生成的 map 使用的是 equals 對象和 hashcode 方法。這意味著你不能使用哈希值容易改變的對象,或者說你無法再找回關(guān)聯(lián)值。

另外值得一提的是,你永遠(yuǎn)不應(yīng)使用 GString 作為 map 的鍵,因為 GString 的哈希值跟相應(yīng)的 String 的哈希值是不同的。

def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
鍵、值與項

這里集中審視一下鍵、值與項的關(guān)系。

def map = [1:'a', 2:'b', 3:'c']

def entries = map.entrySet()
entries.each { entry ->
  assert entry.key in [1,2,3]
  assert entry.value in ['a','b','c']
}

def keys = map.keySet()
assert keys == [1,2,3] as Set

上述代碼所返回的變異值(map 項、鍵或值)是非常令人失望的,因為該操作的成功直接跟操作的 map 類型有關(guān)。尤其關(guān)鍵的是,Groovy 依靠 JDK 的集合,所以一般無法保證能夠安全地通過 keySet、entrySet,或 values 來操控集合。

過濾與搜索

Groovy development kit 所包含的過濾、搜索及收集方法跟 lists 中的差不多。


def people = [
    1: [name:'Bob', age: 32, gender: 'M'],
    2: [name:'Johnny', age: 36, gender: 'M'],
    3: [name:'Claire', age: 21, gender: 'F'],
    4: [name:'Amy', age: 54, gender:'F']
]

def bob = people.find { it.value.name == 'Bob' } // 查找單獨的一個項
def females = people.findAll { it.value.gender == 'F' }

// 都能返回項,但可以使用 `collect` 來獲取一些信息,比如ages
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
    it.value.age
}

assert ageOfBob == 32
assert agesOfFemales == [21,54]

// 但你也可以使用鍵/值對作為閉包的參數(shù)  
def agesOfMales = people.findAll { id, person ->
    person.gender == 'M'
}.collect { id, person ->
    person.age
}
assert agesOfMales == [32, 36]

// 如果所有項都匹配謂語,`every` 就返回 true 
assert people.every { id, person ->
    person.age > 18
}

// 如果所有項都匹配謂語,`any` 就返回 true  

assert people.any { id, person ->
    person.age == 54
}   
Grouping

我們利用一些規(guī)則將列表中的各項分組歸入一個新創(chuàng)建的 map 中,如下所示:

assert ['a', 7, 'b', [2, 3]].groupBy {
    it.class
} == [(String)   : ['a', 'b'],
      (Integer)  : [7],
      (ArrayList): [[2, 3]]
]

assert [
        [name: 'Clark', city: 'London'], [name: 'Sharma', city: 'London'],
        [name: 'Maradona', city: 'LA'], [name: 'Zhang', city: 'HK'],
        [name: 'Ali', city: 'HK'], [name: 'Liu', city: 'HK'],
].groupBy { it.city } == [
        London: [[name: 'Clark', city: 'London'],
                 [name: 'Sharma', city: 'London']],
        LA    : [[name: 'Maradona', city: 'LA']],
        HK    : [[name: 'Zhang', city: 'HK'],
                 [name: 'Ali', city: 'HK'],
                 [name: 'Liu', city: 'HK']],
]

2.3. 范圍(Ranges)

利用范圍可以創(chuàng)建一列連續(xù)的值。它們可以像 List 那樣使用,因為 Range 繼承自 java.util.List

.. 標(biāo)記法定義的范圍是全包含的(包含首尾兩個值的列表)。

..< 標(biāo)記法定義的范圍是半包含的,即只含有起始值,而不包含末尾值。

// 全包含范圍
def range = 5..8
assert range.size() == 4
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert range.contains(8)

// 半包含范圍
range = 5..<8
assert range.size() == 3
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert !range.contains(8)

//不使用索引,獲取范圍的末尾值   
range = 1..10
assert range.from == 1
assert range.to == 10

注意,int 類型的范圍實現(xiàn)的效率高,創(chuàng)建了一個輕量級的包含了首尾值的 Java 對象。

任何實現(xiàn)了 java.lang.Comparable 接口用于相互比較的 Java 對象都可以使用范圍??梢杂?next()previous() 來返回后一個/前一個的范圍項。例如,我們可以創(chuàng)建一定范圍的 String 元素:

// 全包含范圍  
def range = 'a'..'d'
assert range.size() == 4
assert range.get(2) == 'c'
assert range[2] == 'c'
assert range instanceof java.util.List
assert range.contains('a')
assert range.contains('d')
assert !range.contains('e')

你還可以利用一個經(jīng)典的 for 循環(huán)來在范圍中迭代:

for (i in 1..10) {
    println "Hello ${i}"
}

但你還可以采用 Groovy 更慣用的風(fēng)格來做,利用 each 方法來迭代一個范圍:

(1..10).each { i ->
    println "Hello ${i}"
}

范圍還可以用在 switch 語句中:

switch (years) {
    case 1..10: interestRate = 0.076; break;
    case 11..25: interestRate = 0.052; break;
    default: interestRate = 0.037;
}

2.4 對集合(collection)的一些語法增強(qiáng)

2.4.1 GPath 支持

對于列表和 map 都支持屬性標(biāo)記法,Groovy 提供了語法糖,使得處理內(nèi)嵌集合變得非常方便,如下例所示:

def listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22]]
assert listOfMaps.a == [11, 21] //GPath 標(biāo)記
assert listOfMaps*.a == [11, 21] //散布點標(biāo)記  

listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // 也適合 null 值
assert listOfMaps*.a == listOfMaps.collect { it?.a } // 等價標(biāo)記  
// 但只收集 非 null 值
assert listOfMaps.a == [11,21]    

2.4.2 散布操作符

散布操作符還可用于將一個集合內(nèi)聯(lián)到另一個中。它是一個語法糖,因為通過它能夠避免調(diào)用 putAll,有利于實現(xiàn)單行方式。

assert [ 'z': 900,
         *: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
// 在 map 定義中 散布 map 標(biāo)記法
assert [*: [3: 3, *: [5: 5]], 7: 7] == [3: 3, 5: 5, 7: 7]

def f = { [1: 'u', 2: 'v', 3: 'w'] }
assert [*: f(), 10: 'zz'] == [1: 'u', 10: 'zz', 2: 'v', 3: 'w']
//在函數(shù)參數(shù)中的散布 map 標(biāo)記法
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30

f = { m, i, j, k -> [m, i, j, k] }
//使用散布 map 標(biāo)記法來處理未命名與已命名參數(shù)  
assert f('e': 100, *[4, 5], *: ['a': 10, 'b': 20, 'c': 30], 6) ==
        [["e": 100, "b": 20, "c": 30, "a": 10], 4, 5, 6]

2.4.3 星號(*)操作符

通過星號操作符這個快捷操作符,你可以對集合中所有的元素調(diào)用方法或?qū)傩裕?

assert [1, 3, 5] == ['a', 'few', 'words']*.size()

class Person {
    String name
    int age
}
def persons = [new Person(name:'Hugo', age:17), new Person(name:'Sandra',age:19)]
assert [17, 19] == persons*.age

2.4.4 利用下標(biāo)操作符進(jìn)行截取操作

對列表、數(shù)組及 map,使用下標(biāo)表達(dá)式,可以按索引訪問。有趣的是,在這種情況下,字符串也可以被看成一種特殊的集合:

def text = 'nice cheese gromit!'
def x = text[2]

assert x == 'c'
assert x.class == String

def sub = text[5..10]
assert sub == 'cheese'

def list = [10, 11, 12, 13]
def answer = list[2,3]
assert answer == [12,13]

注意,也可以使用范圍來抽取部分集合:

list = 100..200
sub = list[1, 3, 20..25, 33]
assert sub == [101, 103, 120, 121, 122, 123, 124, 125, 133]

下標(biāo)操作符還可以用來更新一個已存在的(可改變)集合:

list = ['a','x','x','d']
list[1..2] = ['b','c']
assert list == ['a','b','c','d']

值得一提的是,允許出現(xiàn)負(fù)數(shù),從而方便了從集合末尾起提取元素。

可以利用負(fù)數(shù)從列表、數(shù)組及字符串等結(jié)構(gòu)的末尾起開始計數(shù):

text = "nice cheese gromit!"
x = text[-1]
assert x == "!"

def name = text[-7..-2]
assert name == "gromit"

最后,如果你使用了反向的范圍(起始索引大于末尾索引),所得結(jié)果也是反的。

text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"

2.5 增強(qiáng)的集合方法

除了列表、map范圍之外,Groovy 還提供了大量的額外方法來實現(xiàn)過濾、收集、組織、計算等操作,這些方法都可以直接適用于集合,或者是更簡單的實現(xiàn)了 iterable 接口的類。

關(guān)于更多細(xì)節(jié),可以閱讀 Groovy development kit API 文檔,尤其是以下部分內(nèi)容:

3. 實用工具

3.1 ConfigSlurper

ConfigSlurper 是一種能夠讀取 Groovy 腳本形式配置文件的工具類。就像與 Java 中 *.properties 文件那樣,ConfigSlurper 可以使用點標(biāo)記法。但除此之外,它還能支持閉包范圍的配置值以及任意對象類型。

def config = new ConfigSlurper().parse('''
    app.date =
上一篇:安裝 Groovy下一篇:設(shè)計模式