about 4 years ago

印象中,在官方的《Java Magazine》雙月刊中,已經探討過好幾回的Java 8及Lambda,但似乎鮮少討論到Closure,稍微快速翻找一下,在2013的七八月號上有看到介紹Lambda的文章中討論到Closure。我想主要的原因應該是(1) 在沒有Lambda前,anonymous class本身就可以捕捉變數,只是捕捉的變數自動被視為final;(2) Java的Lambda所捕獲的變數,某種程度還是不夠自由。無法完整重現自由變數,所以沒有特別去宣傳Java Lambda和Closure之間的關係。翻了一下Wikipedia上對 Closure的描述(我想參考Wikipedia應該比參考某些專論programming languages的書要方便一些),在Closure內可以對自由變數的做任何變數上的操作,包含更動數值,這一點在許多語言的支援上也不見得完全相同。

以最近工作上常寫的Objective C來說,Apple官方文件給了幾個例子,官方文件以儲存空間的角度去探討捕捉變數,這是對記憶體位置特別敏感的語言某種程度上的痛處,不過這裡就用比較抽象的方式解釋code block對於捕捉變數的處理。預設上,code block捕捉變數的值,也就是說在捕捉後對變數的更動在code block內是看不見的,所以Code List 1中NSLog所顯示的結果是42而不是84

Code List 1 - The code block that captures the "anInteger" variable without __block
int anInteger = 42;
SimpleCallback callback = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
callback();

若希望code block捕捉變數而不是僅僅數值的話,需像Code List 2在被捕捉的變數宣告上加上__block的修飾字,此時NSLog顯示的結果就會是84,因為當code block執行時(第6行),捕捉的變數anInteger已經變成84了(第5行)。

Code List 2 - The code block that captures the "anInteger" variable with __block
__block int anInteger = 42;
SimpleCallback callback = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
callback();

__block修飾字讓code block捕捉變數本身,所以也可以更動變數的值,因此Code List 3中NSLog是在callback()執行後才顯示anInteger的值,結果是100。我個人覺得這樣的處理有好有壞,就抽象程度上,額外需要__block修飾字讓工程師還是意識到記憶體位置的存在,這是降低語言的抽象程度(不直覺)。Objective C可用這些修飾字針對效能提升最佳化編譯結果,不但能同時提供唯讀/讀寫的捕捉變數,另外也提供編譯期間的檢查,例如沒用__block但卻在code block中變更變數值會視為錯誤,某種程度上我覺得是還不錯的設計。

Code List 3 - The code block that changes the value of the captured variable
__block int anInteger = 42;
SimpleCallback callback = ^{
    anInteger = 100;
};
callback();
NSLog(@"Value of original variable is now: %i", anInteger);

好,該回到Java本身了,沒有Lambda前,anonymous class可以像Code List 4那樣捕捉scope中可見的變數x,但最大問題是捕捉的變數x實際上是final變數(effectively final),所以被註解的x = 48;若取消註解會被視為編譯錯誤。

Code List 4 - The captured variable in the anonymous class
int x = 24;
Runnable runnable = new Runnable() {

    @Override
    public void run() {
        // x = 48;

        System.out.println(String.format("captured x: %s", x));
    }
};
runnable.run();

即使改成用Lambda也是一樣的,如Code List 5,在Lambda內x依舊是final變數,無法更動變數值,取消x = 48;的註解依然會是編譯錯誤。

Code List 5 - The captured variable in the Lambda expression is still effectively final
int x = 24;
Runnable runnable = () -> {
    // x = 48;

    System.out.println(String.format("captured x: %s", x));
};
runnable.run();

Java的final修飾字僅限制無法改變變數值,但若變數是個物件,呼叫物件method卻是允許的,即使該method會改變物件內的狀態都是允許的,所以Code List 4可以改寫成Code List 6。同樣,取消x = new AtomicInteger(48);的註解會得到編譯錯誤,但用x.set(48);可以實際改變x的值。(這一點Objective C也是一樣)

Code List 6 - The captured object in the anonymous class
AtomicInteger x = new AtomicInteger(24);
Runnable runnable = new Runnable() {

    @Override
    public void run() {
        System.out.println(String.format("original x: %s", x));
        // x = new AtomicInteger(48);

        x.set(48);
    }
};
runnable.run();
System.out.println(String.format("x after run(): %s", x));

所以,Code List 5也可以改寫成Code List 7。很可惜,能夠Autoboxing and Unboxing的資料型態,例如:Integer,都是immutable的資料型態,使用上無法像JavaScript這類將基礎型別都視為物件的語言那樣方便。不過,某種程度上有點像自由變數了。

Code List 7 - The captured object in the Lambda expression
AtomicInteger x = new AtomicInteger(24);
Runnable runnable = () -> {
    System.out.println(String.format("original x: %s", x));
    // x = new AtomicInteger(48);

    x.set(48);
};
runnable.run();
System.out.println(String.format("x after run(): %s", x));

最後,Java 8雖然支援Lambda,但我覺得Closure某種程度上還不稱不上是Java的第一級居民,而且如前篇所述,為了方便測試,除非是只有一行或是非常簡單的程式碼(too simple to break)不用擔心測試的問題外,我還是比較喜歡寫一些小而易測的class,而不是使用Lambda,至於捕捉變數,透過建構子將變數帶入物件也是一種方式。至於什麼情況下會常寫只有一行或是非常簡單的Lambda呢?我覺得Stream,新的Collection API開啟了相當大的可能性。

← Quick Glance of Java 8 - Lambda Quick Glance of Java 8 - Closure →