一. 認(rèn)識(shí)閉包
將代碼塊作為方法參數(shù)進(jìn)行傳遞,這種機(jī)制就叫做閉包。閉包可以引用在創(chuàng)建閉包的范圍中可見的變量。最近關(guān)于閉包的討論也比較多,閉包能使語(yǔ)言更具靈動(dòng)性,在動(dòng)態(tài)腳本語(yǔ)言中較廣泛的支持,如 Perl、Python、Ruby、JavaScript,還有我們的 Groovy。
有些語(yǔ)言能把函數(shù)作為參數(shù)傳遞,如 JavaScript 的回調(diào)函數(shù),Python,甚至是 C++ 的函數(shù)指針。而 Java 在這方面又略遜一籌,需搬動(dòng)一個(gè)匿名的內(nèi)部類來實(shí)現(xiàn)類似的功能,內(nèi)部類只能訪問外部聲明為 final 的變量。不過有呼聲要在 Java SE 7 中增加閉包特性,讓我們?cè)嚹恳源伞?/p>
Groovy 這回大概是從 Ruby 那兒偷得閉包的語(yǔ)法。前面說這么多,其實(shí)你看到了就會(huì)發(fā)現(xiàn),其實(shí)閉包很簡(jiǎn)單的,不信,請(qǐng)看:
- logo = {
- println "Closure";
- }
- logo.call();
- logo();
用大括號(hào)括起來的,給它一個(gè)名字 logo 的那段就是一個(gè)閉包(有點(diǎn)像 Java 中的語(yǔ)句塊);閉包可以通過執(zhí)行它的 call() 方法調(diào)用,或者直接把它當(dāng)作一個(gè)常規(guī)方法對(duì)待。閉包也可以沒有名字,比如下面要講的集合方法中用的閉包。
二. 閉包的參數(shù)
閉包可以有參數(shù)。如果是一個(gè)參數(shù)的話,該參數(shù)就直接映射到名為 it 的變量,如:
- discount = { it * 0.8};
- println discount(200);
如果是多個(gè)參數(shù)的閉包,則在閉包中用 "->" 把參數(shù)列表和實(shí)現(xiàn)隔開,如:(當(dāng)然一個(gè)參數(shù)也可以這么方式定義的)
- totalPrice = {subtotal, tax, discount ->
- subtotal * discount * (1+tax);
- }
- println totalPrice(100,0.2,0.3);
我看到這里,怎么越來越覺得閉包那么像 C/C++ 中的宏定義呢?
調(diào)用閉包時(shí),如果參數(shù)數(shù)量不對(duì)會(huì)拋出 IncorrectClosureArgumentException;不過對(duì)于一個(gè)參數(shù)的閉包,可以少傳但不能多傳參數(shù),不傳參數(shù)時(shí),閉包認(rèn)為 it 為 null。
三. 閉包的傳遞
閉包在實(shí)現(xiàn)上是擴(kuò)展自 groovy.lang.Closure 類,因?yàn)樗鼈兪穷?,所以可以作為參?shù)傳遞給其他的方法,下面就來看一個(gè)例子:
- class Handler{
- def action;
- def handle(object){
- action(object);
- }
- }
-
-
- log = {object->println "Action occured: ${object}"};
- save ={object->println "Object saved to the database: ${object}"};
-
- logHandler = new Handler(action:log);
- saveHandler = new Handler(action:save);
-
- obj = "Status changed";
- logHandler.handle(obj);
- saveHandler.handle(obj);
輸出為:
Action occured: Status changed
Object saved to the database: Status changed
上面這種用法可用在事件觸發(fā)、回調(diào)操作或策略模式中。
四. 閉包與變量作用域
在 Groovy 中閉包可以訪問創(chuàng)建它的上下文中定義的變量,可在閉包中修改;而且閉包內(nèi)部定義的變量在周圍的上下文中也是可見的。看如下代碼片斷:
- tax = 0.2;
- c1 = {
- tax += 0.1;
- discount = 0.2;
- }
-
- c1();
- println tax;
- println discount;
執(zhí)行的結(jié)果是:
0.3
0.2
五. 閉包與集合操作
當(dāng)閉包與集合整合時(shí),它們展現(xiàn)了真正的威力。Groovy 中的 List、Map、String、和 Range 都有接受閉包參數(shù)的額外方法,譬如字符串也是字符的集合,也可以這么用。
涉及到的集合操作有 each、collect、inject、find、findAll、every、any。下面以例子來幫助理解這些方法的使用。
- [1,2,3].each {print it+1};
-
- ["Name":"Unmi","Skill":"Java"].each{
- print "${it.key}: ${it.value} "
- };
-
- "Groovy".each {print it.toUpperCase()};
-
- (1..3).each {print it+1};
注意:上面的閉包傳給方法,如果是在 GroovyShell 中逐行敲入代碼時(shí),起始花括號(hào)"{" 必須與調(diào)用方法在同一行上,比如說寫成下面的方式就有問題:
- [1,2,3].each
- {
- print it+1
- }
然而如果你想要在單獨(dú)一行上指定起始花括號(hào),可以使用下語(yǔ)法(調(diào)用方法后加個(gè)圓括號(hào)):
- [1,2,3].each (
- {
- print it+1
- }
- )
要是寫在 .groovy 文件中或是在 GroovyConsole 中不受這個(gè)限制,但是為規(guī)范和不致?lián)Q個(gè)環(huán)境又出錯(cuò),還是應(yīng)該讓起起始花括號(hào)"{" 與調(diào)用方法在同一行。
其實(shí)怎么去理解這種要求呢?主要是兩點(diǎn):
其一是為什么我們通常不寫這個(gè)圓括呢?那是 Groovy 允許方法調(diào)用時(shí)省略圓括號(hào)
還有就是在 GroovyShell 下,如果輸完方法名,如 each,然后馬上回車,就報(bào)錯(cuò)
ERROR groovy.lang.MissingPropertyException: Exception evaluating property 'each' for java.util.ArrayList, Reason: groovy
.lang.MissingPropertyException: No such property: each for class: java.lang.Integer
at groovysh_evaluate.run (groovysh_evaluate:1)
...
只有方法名后面加個(gè)圓括號(hào),它才知道是參數(shù)列表開始,直至匹配的右圓括號(hào)結(jié)束,或是加了花括,它也知道是一個(gè)閉包的開始,直到匹配的花括號(hào)結(jié)束。
1) collect() 方法利用指定的閉包轉(zhuǎn)換集合的元素
- println ([1,2,3].collect{it*2});
2) inject() 方法,可將前一次的迭代的結(jié)果傳遞給下一次迭代,請(qǐng)看例子:
- [1,2,3].inject 0, {prevItem,item ->
- println "Previous: ${prevItem} - Current: ${item}"
- return item;
- }
輸出為:
Previous: 0 - Current: 1
Previous: 1 - Current: 2
Previous: 2 - Current: 3
在當(dāng)前迭代中能知道上一次所迭代的值。inject 接受兩個(gè)參數(shù),第一次迭代中使用的注入值,和將要使用的閉包。這個(gè)閉包也必須定義兩個(gè)參數(shù),分別為上次迭代的注入值和當(dāng)前的元素。注意在閉包中必須返回下次迭代中注入的值。否則,就會(huì)假設(shè)為 null。
為了現(xiàn)好的理解發(fā)這個(gè)注入語(yǔ)法,我們將上述例子分開來寫成如下:
- list = [1,2,3]
- closure = {prevItem,item ->
- println "Previous: ${prevItem} - Current: ${item}"
- return item;
- }
- list.inject(0,closure)
3) find() 方法找到符合閉包中所定義條件的第一次出現(xiàn)的元素,找不到則返回 null
- println([2,5,7,9].find { it>5 });
4) findAll() 方法是返回所有符合閉包中所定義條件的元素所組成的集合
- println([2,5,7,9].findAll { it>5 });
5) every() 方法檢查集合中是否每一個(gè)元素都符合閉包中指定的條件,是則返回 true,否則為 false
- println([2,5,7,9].every { it>1 });
6) any() 方法則檢查集合中是否有一個(gè)元素符合閉包中指定的條件,有則返回 true,否則為 false
- println([2,5,7,9].any { it>8 });
參考:1. 《Java 腳本編程語(yǔ)言、框架與模式》第 4 章