java 匿名函数_Java 理论与实践,闭包之争
Java 語(yǔ)言是否應(yīng)增加閉包以及如何添加?
在跨越邊界 系列最近的一篇文章中,我的朋友兼同事 Bruce Tate 以 Ruby 為例描述了閉包的強(qiáng)大功能。最近在安特衛(wèi)普召開的 JavaPolis 會(huì)議上,聽眾人數(shù)最多的演講是 Neal Gafter 的 “向 Java 語(yǔ)言增加閉包特性”。在 JavaPolis 的公告欄上,與會(huì)者可以寫下和 Java 技術(shù)有關(guān)(或者無關(guān))的想法,其中將近一半和關(guān)于閉包的爭(zhēng)論有關(guān)。最近似乎 Java 社區(qū)的每個(gè)人都在討論閉包——雖然閉包這一業(yè)已成熟的概念早在 Java 語(yǔ)言出現(xiàn)的 20 年之前就已經(jīng)存在了。
本文中,我的目標(biāo)是介紹關(guān)于 Java 語(yǔ)言閉包特性的種種觀點(diǎn)。本文首先介紹閉包的概念及其應(yīng)用,然后簡(jiǎn)要說明目前提出來的相互競(jìng)爭(zhēng)的一些方案。
閉包:基本概念
閉包是可以包含自由(未綁定)變量的代碼塊;這些變量不是在這個(gè)代碼塊或者任何全局上下文中定義的,而是在定義代碼塊的環(huán)境中定義。“閉包” 一詞來源于以下兩者的結(jié)合:要執(zhí)行的代碼塊(由于自由變量的存在,相關(guān)變量引用沒有釋放)和為自由變量提供綁定的計(jì)算環(huán)境(作用域)。在 Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby 和 Python 等語(yǔ)言中都能找到對(duì)閉包不同程度的支持。
閉包的價(jià)值在于可以作為函數(shù)對(duì)象 或者匿名函數(shù),對(duì)于類型系統(tǒng)而言這就意味著不僅要表示數(shù)據(jù)還要表示代碼。支持閉包的多數(shù)語(yǔ)言都將函數(shù)作為第一級(jí)對(duì)象,就是說這些函數(shù)可以存儲(chǔ)到變量中、作為參數(shù)傳遞給其他函數(shù),最重要的是能夠被函數(shù)動(dòng)態(tài)地創(chuàng)建和返回。比如下面清單 1 所示的 Scheme 例子(摘自 SICP 3.3.3):
清單 1. Scheme 編程語(yǔ)言的函數(shù)示例,該函數(shù)接受另一個(gè)函數(shù)作為參數(shù)并返回緩存后的函數(shù)
(define (memoize f) (let ((table (make-table))) (lambda (x) (let ((previously-computed-result (lookup x table))) (if (not (null? previously-computed-result)) previously-computed-result (let ((result (f x))) (insert! x result table) result))))))上述代碼定義了一個(gè)叫做 memoize 的函數(shù),接受函數(shù) f 作為其參數(shù),返回和 f 計(jì)算結(jié)果相同的另一個(gè)函數(shù),不過新函數(shù)將以前的計(jì)算結(jié)果保存在表中,這樣讀取結(jié)果更快。返回的函數(shù)使用 lambda 結(jié)構(gòu)創(chuàng)建,該結(jié)構(gòu)動(dòng)態(tài)創(chuàng)建新的函數(shù)對(duì)象。斜體顯示的標(biāo)識(shí)符在新定義函數(shù)中是自由的,它們的值在創(chuàng)建該函數(shù)的環(huán)境中綁定。比如,用于存儲(chǔ)緩存數(shù)據(jù)的表變量在調(diào)用 memoize 的時(shí)候創(chuàng)建,由于被新建的函數(shù)引用,因此直到垃圾回收器回收結(jié)果函數(shù)的時(shí)候才會(huì)被收回。如果調(diào)用結(jié)果函數(shù)時(shí)帶有參數(shù) x ,它首先檢查是否已經(jīng)計(jì)算過 f(x)。是的話返回已經(jīng)得到的 f(x),否則計(jì)算 f(x) 并在返回之前保存到表中以備后用。
閉包為創(chuàng)建和操縱參數(shù)化的計(jì)算提供了一種緊湊、自然的方式。可以認(rèn)為支持閉包就是提供將 “代碼塊” 作為第一級(jí)對(duì)象處理的能力:能夠傳遞、調(diào)用和動(dòng)態(tài)創(chuàng)建新的代碼塊。要完全支持閉包,這種語(yǔ)言必須支持在運(yùn)行時(shí)操縱、調(diào)用和創(chuàng)建函數(shù),還要支持函數(shù)可以捕獲創(chuàng)建這些函數(shù)的環(huán)境。很多語(yǔ)言僅提供了這些特性的一個(gè)子集,具備閉包的部分但不是全部?jī)?yōu)勢(shì)。關(guān)于是否要在 Java 語(yǔ)言中增加閉包,關(guān)鍵問題在于提高表達(dá)能力所帶來的益處能否與更高的復(fù)雜性所帶來的代價(jià)相抵消。
匿名類和函數(shù)指針
C 語(yǔ)言提供了函數(shù)指針,允許將函數(shù)作為參數(shù)傳遞給其他函數(shù)。但是,C 中的函數(shù)不能有自由變量:所有變量在編譯時(shí)必須是已知的,這就降低了函數(shù)指針作為一種抽象機(jī)制的表達(dá)能力。
Java 語(yǔ)言提供了內(nèi)部類,可以包含對(duì)封閉對(duì)象字段的引用。該特性比函數(shù)指針更強(qiáng)大,因?yàn)樗试S內(nèi)部類實(shí)例保持對(duì)創(chuàng)建它的環(huán)境的引用。乍看起來,內(nèi)部類似乎確實(shí)提供了閉包的大部分作用,雖然這還不是全部作用。您可以很容易構(gòu)造一個(gè)名為 UnaryFunction 的接口,并創(chuàng)建能夠緩存任何 unary 函數(shù)的緩存包裝程序。但是這種方法通常不易于實(shí)現(xiàn),它要求與函數(shù)交互的所有代碼在編寫時(shí)都必須知道這個(gè)函數(shù)的 “框架”。
閉包作為一種模式模板
匿名類允許創(chuàng)建這樣的對(duì)象,該對(duì)象能夠捕獲定義它們的一部分環(huán)境,但是對(duì)象和代碼塊不一樣。以一個(gè)常見的編碼模式為例,如執(zhí)行帶有 Lock 的代碼塊。如果需要遞增帶有 Lock 的計(jì)數(shù)器,代碼如清單 2 所示——即使這么簡(jiǎn)單的操作也非常羅嗦:
清單 2. 執(zhí)行加鎖代碼塊的規(guī)范用法
lock.lock();try { ++counter;}finally { lock.unlock();}如果能夠提取出加鎖管理代碼就好了,這樣會(huì)使代碼看起來更緊湊,也不容易出錯(cuò)。首先可以創(chuàng)建如清單 3 所示的 withLock() 方法:
清單 3. 提取了 “加鎖執(zhí)行” 的概念,但是問題在于缺乏異常的透明性
public static void withLock(Lock lock, Runnable r) { lock.lock(); try { r.run(); } finally { lock.unlock(); }}不幸的是,這種方法只能達(dá)到您預(yù)期的部分目標(biāo)。創(chuàng)建這種抽象代碼的目標(biāo)之一是使代碼更緊湊;但是,匿名內(nèi)部類的語(yǔ)法不是很緊湊,調(diào)用代碼看起來如清單 4 所示:
清單 4. 清單 3 中 withLock() 方法的客戶端代碼
withLock(lock, new Runnable() { public void run() { ++counter; }});要遞增一個(gè)加鎖的計(jì)數(shù)器仍然需要編寫很多代碼!另外,將受到鎖保護(hù)的代碼塊轉(zhuǎn)化成方法調(diào)用所帶來的抽象問題大大增加了問題的復(fù)雜性——如果受保護(hù)的代碼塊拋出一個(gè)檢測(cè)異常怎么辦?現(xiàn)在我們不能使用 Runnable 來表示執(zhí)行的任務(wù),而必須創(chuàng)建一種新的表示方法以允許在方法調(diào)用中拋出異常。不幸的是,在這里泛化也幫不上多少忙,雖然方法可以用泛型參數(shù) E, 表示可能拋出的檢測(cè)異常,但是這種方法不能很好地泛化拋出多種檢測(cè)異常類型的方法(這就是為何 Callable 中的 call() 方法聲明為拋出 Exception 而不是用類型參數(shù)指定一個(gè)類型的原因)。清單 3 中的方法最大的問題在于缺乏異常透明性,除此之外,還存在其他非透明性的問題,在 清單 4 的 Runnable 上下文中,return 或 break 這類語(yǔ)句的含義,與 清單 2 中 try 語(yǔ)句塊中的一般意義不同。
理想情況下,受保護(hù)的遞增操作應(yīng)該像清單 5 所示的那樣,并且塊中代碼的含義和 清單 2 的擴(kuò)展形式相同:
清單 5. 清單 3 客戶端代碼的理想形式(但是是假設(shè)形式)
withLock(lock, { ++counter; });在語(yǔ)言中添加閉包以后,就可以創(chuàng)建行為類似控制流結(jié)構(gòu)的方法,比如 “加鎖執(zhí)行這段代碼”、“操作流并在完成后將其關(guān)閉” 或者 “為代碼塊的執(zhí)行計(jì)時(shí)” 等。這種策略有可能簡(jiǎn)化某些類型的代碼,這些代碼反復(fù)使用特定編碼模式或者慣用法,比如 清單 2 所示的加鎖用法。(在一定程度上提供類似表達(dá)能力的另一種技術(shù)是 C 預(yù)處理器,它可以將 withLock() 操作用預(yù)處理宏表示,雖然和閉包相比宏更難以組織,而且安全性也更差。)
泛化算法的閉包
閉包能夠大大簡(jiǎn)化代碼的另一個(gè)地方是泛化算法的使用。隨著多處理器計(jì)算機(jī)越來越便宜,利用小粒度并行機(jī)制的重要性日漸突出。使用泛化算法定義計(jì)算為庫(kù)實(shí)現(xiàn)在問題空間中采用并行機(jī)制提供了一種自然的方式。
比方說,假設(shè)要計(jì)算一個(gè)大型數(shù)字集合的平方和。清單 6 給出了一種計(jì)算方法,但這種方法是按順序計(jì)算結(jié)果的,對(duì)于大規(guī)模多處理器系統(tǒng)可能不是效率最高的方法:
清單 6. 順序計(jì)算平方和
double sum;for (Double d : myBigCollection) sum += d*d;每次循環(huán)迭代有兩個(gè)操作:取平方,累加到最終結(jié)果。平方操作是互相獨(dú)立的,可以并行執(zhí)行;加法操作也不一定要執(zhí)行 N 次,如果計(jì)算組織得當(dāng),只要 log(N) 次操作即可完成。
清單 6 中的操作是 map-reduce 算法的一個(gè)示例,對(duì)大批數(shù)據(jù)元素中的每一個(gè)數(shù)據(jù)元素應(yīng)用一個(gè)函數(shù),然后將每次應(yīng)用該函數(shù)計(jì)算出的結(jié)果通過某種累加函數(shù)累加起來。假設(shè)有一個(gè) map-reduce 實(shí)現(xiàn)過程接受數(shù)據(jù)集作為輸入,用一元函數(shù)處理每個(gè)元素,用二元函數(shù)累加結(jié)果,則可用清單 7 所示的代碼完成平方和運(yùn)算:
清單 7. 使用 MapReduce 計(jì)算平方和,可以實(shí)現(xiàn)并行執(zhí)行
Double sumOfSquares = mapReduce(myBigCollection, new UnaryFunction { public Double apply(Double x) { return x * x; } }, new BinaryFunction { public Double apply(Double x, Double y) { return x + y; } });假設(shè)清單 7 中的 mapReduce() 實(shí)現(xiàn)知道哪些操作可以并行執(zhí)行,因而可以將函數(shù)應(yīng)用和累加過程并行執(zhí)行,從而改進(jìn)并行系統(tǒng)的吞吐量。但是清單 7 中的代碼不簡(jiǎn)潔,用了更多代碼來表達(dá)和清單 6 中三行代碼等價(jià)的泛化算法。
通過閉包可以更好地管理清單 7 中的代碼。比如,清單 8 中的閉包語(yǔ)法和目前提出的 Java 語(yǔ)言閉包方案都不一樣,目的僅在于說明閉包對(duì)泛化算法的支持:
清單 8. 使用 MapReduce 和假設(shè)的閉包語(yǔ)法計(jì)算平方和
sumOfSquares = mapReduce(myBigCollection, function(x) {x * x}, function(x, y) {x + y});清單 8 中基于閉包的算法具有兩方面的好處:代碼容易閱讀和編寫,抽象層次比順序循環(huán)更高,能夠有效地通過庫(kù)實(shí)現(xiàn)并行。
閉包方案
目前至少提出了兩種向 Java 語(yǔ)言增加閉包的方案。其一,綽號(hào)為 “BGGA”(名字源于其作者 Gilad Bracha、Neal Gafter、James Gosling 和 Peter von der Ahe),它擴(kuò)展了類型系統(tǒng),引入了 function 類型。其二,綽號(hào)為 “CICE” (代表 Concise Inner Class Expressions,簡(jiǎn)潔內(nèi)部類表示),是由 Joshua Bloch、Doug Lea 和 “瘋狂的” Bob Lee 所支持的,其目標(biāo)更謙虛:簡(jiǎn)化匿名內(nèi)部類實(shí)例的創(chuàng)建。 JSR 可能很快就會(huì)收到這方面的提議,考慮在未來的 Java 語(yǔ)言版本中支持閉包的形式和程度。
BGGA 方案
BGGA 方案提出了 function 類型的概念,即函數(shù)都帶有一個(gè)類型參數(shù)列表、返回類型和 throws 子句。在 BGGA 方案中,計(jì)算平方和的代碼將如清單 9 所示:
清單 9. 使用 BGGA 閉包語(yǔ)法計(jì)算平方和
sumOfSquares = mapReduce(myBigCollection, { Double x => x * x }, { Double x, Double y => x + y });=> 字符到左側(cè)花括號(hào)之間的代碼表示參數(shù)的名稱和類型,右側(cè)的代碼表示定義的匿名函數(shù)的實(shí)現(xiàn)。這段代碼可以引用塊中定義的局部變量、閉包的參數(shù)以及創(chuàng)建閉包的作用域中的變量。
在 BGGA 方案中,可以聲明 function 類型的變量、方法參數(shù)和方法返回值。在需要一個(gè)抽象方法類(如 Runnable 或 Callable)實(shí)例的任何上下文中都可以使用閉包,對(duì)于匿名類型的閉包,您可以使用帶有給定參數(shù)列表的 invoke() 方法來調(diào)用。
BGGA 方案的主要目標(biāo)之一是允許程序員創(chuàng)建行為類似控制結(jié)構(gòu)的方法。因此,BGGA 還在語(yǔ)法上提出了一些吸引人的花招,允許像新的關(guān)鍵字那樣調(diào)用接受閉包的方法,從而能夠創(chuàng)建像 withLock() 或 forEach() 這樣的方法,然后向控制原語(yǔ)一樣調(diào)用它們。清單 10 說明了根據(jù) BGGA 方案如何定義 withLock() 方法,清單 11 和 清單 12 說明了如何調(diào)用該方法,包括標(biāo)準(zhǔn)形式和“控制結(jié)構(gòu)”形式:
清單 10. 采用 BGGA 閉包方案編寫的 withLock() 方法
public static T withLock(Lock lock, {=>T throws E} block) throws E { lock.lock(); try { return block.invoke(); } finally { lock.unlock(); }}清單 10 中的 withLock() 方法接受鎖和閉包。閉包的返回類型和 throws 子句是泛化參數(shù),編譯器中的類型推斷通常允許在未指定 T 和 E 值的情況下調(diào)用,如清單 11 和 12 所示:
清單 11. 調(diào)用 withLock()
withLock(lock, {=> System.out.println("hello");});清單 12. 使用控制結(jié)構(gòu)的縮寫形式調(diào)用 withLock()
withLock(lock) { System.out.println("hello");}和泛化一樣,BGGA 方案中閉包的復(fù)雜性在很大程度上是由庫(kù)的編寫者來分擔(dān)的,使用接受閉包的庫(kù)方法更簡(jiǎn)單。
使用內(nèi)部類實(shí)例是閉包所帶來的好處,但是這種方法缺少透明性,BGGA 方案在一定程度上還有助于解決這個(gè)問題。比如,return、 break 和 this 在某一代碼塊中的語(yǔ)義與其在 Runnable(或其他內(nèi)部類實(shí)例)中同一代碼塊中的語(yǔ)義是不同的。為了利用泛化算法而對(duì)代碼進(jìn)行移值的時(shí)候,這些不透明因素可能會(huì)造成混亂。
CICE 方案
CICE 方案要簡(jiǎn)單得多,它解決了實(shí)例化內(nèi)部類實(shí)例不太靈活的問題。它沒有建立函數(shù)類型的概念,只不過為一個(gè)抽象方法(如 Runnable、Callable 或 Comparator)內(nèi)部類實(shí)例化提出了一種更緊湊的語(yǔ)法。
清單 13 說明了按照 CICE 如何計(jì)算平方和。它顯示使用了 mapReduce() 中的 UnaryFunction 和 BinaryFunction 類型。mapReduce() 的參數(shù)是從 UnaryFunction 和 BinaryFunction 派生的匿名類,這種語(yǔ)法大大了降低了創(chuàng)建匿名實(shí)例的冗余。
清單 13. 采用 CICE 閉包方案計(jì)算平方和的代碼
Double sumOfSquares = mapReduce(myBigCollection, UnaryFunction(Double x) { return x*x; }, BinaryFunction(Double x, Double y) { return x+y; });由于為傳遞給 mapReduce() 的函數(shù)所創(chuàng)建的對(duì)象是普通的匿名類實(shí)例,其函數(shù)體可以引用封閉域中定義的變量,清單 13 中的方法和清單 7 相比,唯一的區(qū)別在于語(yǔ)法的繁簡(jiǎn)程度。
結(jié)束語(yǔ)
BGGA 方案為 Java 這種語(yǔ)言增加了功能強(qiáng)大的新武器,但是同時(shí)也為其語(yǔ)義和語(yǔ)法帶來了可以預(yù)見的復(fù)雜性。另一方面,CICE 方案更簡(jiǎn)單:利用語(yǔ)言中已有的特性并使其更易于使用,但是沒有增加重要的新功能。閉包是一種強(qiáng)大的抽象機(jī)制,用過之后多數(shù)人不愿意放棄。(問問那些熟悉 Scheme、Smalltalk 或 Ruby 編程的朋友對(duì)閉包的感想如何,他們可能會(huì)反問您對(duì)呼吸有什么感想。)但語(yǔ)言是有機(jī)的整體,為語(yǔ)言增加最初設(shè)計(jì)時(shí)沒有預(yù)料到的新特性充滿了危險(xiǎn),而且會(huì)增加語(yǔ)言的復(fù)雜性。爭(zhēng)論的焦點(diǎn)不在于閉包是否有用——因?yàn)榇鸢革@然是肯定的——而在于為閉包重新改造 Java 語(yǔ)言的好處是否抵得上要付出的代價(jià)。
總結(jié)
以上是生活随笔為你收集整理的java 匿名函数_Java 理论与实践,闭包之争的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: a jni error has occu
- 下一篇: 好男人都结婚了吗?最后的研究结论亮了……