此為過去的舊文,2014 年 3 月 30 日初次發表於 logdown。
印象中,在官方的《Java Magazine》雙月刊中,已經探討過好幾回的 Java 8 及 Lambda,但似乎鮮少討論到 Closure,稍微快速翻找一下,在 2013 的七八月號上有看到介紹 Lambda 的文章中討論到Closure。我想主要的原因應該是
翻了一下 Wikipedia 上對 Closure 的描述 (我想參考 Wikipedia 應該比參考某些專論programming languages的書要方便一些),在 Closure 內可以對自由變數的做任何變數上的操作,包含更動數值,這一點在許多語言的支援上也不見得完全相同。
以最近工作上常寫的 Objective C 來說,Apple 官方文件給了幾個例子,官方文件以儲存空間的角度去探討捕捉變數,這是對記憶體位置特別敏感的語言某種程度上的痛處,不過這裡就用比較抽象的方式解釋 code block 對於捕捉變數的處理。預設上,code block 捕捉變數的值,也就是說在捕捉後對變數的更動在 code block 內是看不見的,所以下例中 NSLog
所顯示的結果是 42
而不是 84
。
若希望 code block 捕捉變數而不是僅僅數值的話,需像下例在被捕捉的變數宣告上加上 __block
的修飾字,此時 NSLog
顯示的結果就會是 84
,因為當 code block 執行時 (第6行),捕捉的變數 anInteger
已經變成 84
了 (第5行)。
__block
修飾字讓 code block 捕捉變數本身,所以也可以更動變數的值,因此下例中 NSLog
是在 callback()
執行後才顯示 anInteger
的值,結果是 100
。我個人覺得這樣的處理有好有壞,就抽象程度上,額外需要 __block
修飾字讓工程師還是意識到記憶體位置的存在,這是降低語言的抽象程度(不直覺)。
Objective C 可用這些修飾字針對效能提升最佳化編譯結果,不但能同時提供唯讀/讀寫的捕捉變數,另外也提供編譯期間的檢查,例如沒用 __block
但卻在 code block 中變更變數值會視為錯誤,某種程度上我覺得是還不錯的設計。
好,該回到 Java 本身了,沒有 Lambda 前,anonymous class 可以像下例那樣捕捉 scope 中可見的變數 x
,但最大問題是捕捉的變數 x
實際上是 final 變數 (effectively final),所以被註解的 x = 48;
若取消註解會被視為編譯錯誤。
即使改成用 Lambda 也是一樣的,如下例,在 Lambda 內 x
依舊是 final 變數,無法更動變數值,取消 x = 48;
的註解依然會是編譯錯誤。
Java 的 final
修飾字僅限制無法改變變數值,但若變數是個物件,呼叫物件 method 卻是允許的,即使該 method 會改變物件內的狀態都是允許的,所以 anonymous class 的範例可以改寫如下。同樣,取消 x = new AtomicInteger(48);
的註解會得到編譯錯誤,但用 x.set(48);
可以實際改變 x
的值 (這在 Objective C 也是一樣)。
所以,Lambda 的例子也可以改寫如下。很可惜,能夠 Autoboxing and Unboxing 的資料型態,例如:Integer
,都是 immutable 的資料型態,使用上無法像 JavaScript 這類將基礎型別都視為物件的語言那樣方便。不過,某種程度上有點像自由變數了。
最後,Java 8 雖然支援 Lambda,但我覺得 Closure 某種程度上還不稱不上是 Java 的第一級居民,而且如Java 8 初探 - Lambda所述,為了方便測試,除非是只有一行或是非常簡單的程式碼 (too simple to break) 不用擔心測試的問題外,我還是比較喜歡寫一些小而易測的 class,而不是使用 Lambda,至於捕捉變數,透過建構子將變數帶入物件也是一種方式。至於什麼情況下會常寫只有一行或是非常簡單的 Lambda 呢?我覺得 Stream,新的 Collection API 開啟了相當大的可能性。