Java 8 Lambda限制:閉包

ijxx4750 7年前發布 | 16K 次閱讀 Lambda 閉包 Java8 Java開發

假設我們想創建一個簡單的線程,只在控制臺上打印一些東西:

int answer = 42;
Thread t = new Thread(
    () -> System.out.println("The answer is: " + answer)
);

如果我們想在線程里面修改answer的值怎么辦?

在本文中,我想回答這個問題,討論Java lambda表達式的限制和沿途的后果。

簡單的答案是Java實現閉包,但是當我們將它們與其他語言進行比較時會有限制。另一方面,這些限制可以被認為是可忽略的。

為了支持這種說法,我將展示閉包在JavaScript這一著名語言中起著至關重要的作用。

Java 8 Lambda表達式從哪里來?

在過去,實現上述示例的緊湊方法是創建一個新的Runnable匿名類的實例,如下所示:

int answer = 42;
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("The answer is: " + answer);
    }
});

從Java 8開始,上一個例子可以使用lambda表達式編寫。

現在,我們都知道Java 8 lambda表達式不僅僅是為了降低代碼的冗長性,他們還有很多其他的新功能。此外,在匿名類和lambda表達式的實現之間存在差異。

但是,主要的一點我想在此強調的是,考慮到他們在封閉范圍如何交互,我們可以認為它們只是一種創建匿名類接口的緊湊方式,比如  Runnable ,  Callable ,  Function ,  Predicate ,等。實際上,lambda表達式和它的封閉范圍之間的相互作用保持完全相同(即this 關鍵字語義上的差異  )。

Java 8 Lambda限制

Java中的lambda表達式(以及匿名類)只能訪問封閉范圍的最終(或實際上最終)變量。

例如,考慮以下示例:

void fn() {
    int myVar = 42;
    SupplierlambdaFun = () -> myVar; // error
    myVar++;
    System.out.println(lambdaFun.get());
}

這不會編譯,因為增量myVar阻止它是實際上最終變量。

JavaScript及其功能

JavaScript中的函數和lambda表達式使用閉包的概念:

“閉包是一種特殊類型的對象,它結合了兩個東西:一個函數,以及創建該函數的環境。環境包括在創建閉包時在范圍內的任何局部變量” – MDN

事實上,前面的例子在JavaScript中工作得很好。

function fn() { // the enclosing scope
    var myVar = 42;
    var lambdaFun = () => myVar;
    myVar++;
    console.log(lambdaFun()); // it prints 43
}

此示例中的lambda函數使用的已更改值的myVar。

實際上,在JavaScript中,一個新函數維護一個指向它所定義的封閉范圍的指針。這個基本機制允許創建閉包,這保存了自由變量的存儲位置 – 這些可以由函數本身以及其他函數修改。

Java創建閉包?

Java只保存自由變量的值,讓它們在lambda表達式中使用。即使有一個增量myVar,lambda函數仍然會返回42.編譯器避免了那些不相干的情況的創建,限制可以在lambda表達式(和匿名類)內部使用的變量的類型只有最終的和實際上最終的。

盡管有這個限制,我們可以使用Java 8實現閉包。事實上,閉包更多的是理論上的理解,只捕獲自由變量的價值。在純函數語言中,這應該是唯一允許的,保持引用透明度屬性。

后來,一些功能語言以及諸如Javascript之類的語言引入了捕獲自由變量的存儲位置的可能性。這允許引入副作用的可能性。

所以,我們可以說,使用JavaScript的閉包,我們可以做更多。但是,這些副作用如何真正幫助JavaScript?他們真的很重要嗎?

副作用和JavaScript

為了更好地理解閉包的概念,現在考慮下面的JavaScript代碼(forgive在JavaScript中,這可以以非常緊湊的方式完成,但我想它看起來像Java并進行比較):

function createCounter(initValue) { // the enclosing scope
    var count = initValue;
    var map = new Map();
    map.set('val', () => count);
    map.set('inc', () => count++);
    return map;
}
v = createCounter(42);
v.get('val')(); // returns 42
v.get('inc')(); // returns 42
v.get('val')(); // returns 43

每次  createCounter 調用時,它都會創建一個具有兩個新lambda函數的映射,它們分別返回和遞增在封閉范圍中定義的變量值。

換句話說,第一個函數具有改變另一個函數的結果的副作用。

這里要注意的一個重要事實是,它createCounter的作用域在它的終止之后仍然存在,并且同時被兩個lambda函數使用。

副作用和Java

現在讓我們嘗試在Java中做同樣的事情:

public static Map<String, Supplier> createCounter(int initValue) { // the enclosing scope
    int count = initValue;
    Map<String, Supplier> map = new HashMap<>();
    map.put("val", () -> count);
    map.put("inc", () -> count++);
    return map;
}

此代碼不會編譯,因為第二個lambda函數試圖更改變量count。

Java將函數變量(例如count)存儲在堆棧中; 那些被刪除與終止createCounter。創建的lambdas使用的復制版本count。如果編譯器允許第二個lambda改變它的復制版本count ,那將是很混亂。

Java閉包使用可變對象

正如我們所看到的,使用的變量的值被復制到lambda表達式(或匿名類)。但是,如果我們使用對象呢?在這種情況下,只有引用將被復制,我們可以看看有點不同的東西。

我們幾乎可以用以下方式模擬JavaScript的閉包的行為:

private static class MyClosure {
    public int value;
    public MyClosure(int initValue) { this.value = initValue; }
}
public static Map<String, Supplier> createCounter(int initValue) {
    MyClosureclosure = new MyClosure(initValue);
    Map<String, Supplier> counter = new HashMap<>();
    counter.put("val", () -> closure.value);
    counter.put("inc", () -> closure.value++);
    return counter;
}
Supplier[] v = createCounter(42);
v.get("val").get(); // returns 42
v.get("inc").get(); // returns 42
v.get("val").get(); // returns 43

事實上,這不是真正有用的東西,它真的是不太優雅。

閉包作為創建對象的機制

JavaScript使用閉包作為創建“類”實例:對象的基本機制。這就是為什么在JavaScript中,類似的函數MyCounter稱為“構造函數”。

相反,Java已經有類,我們可以以更優雅的方式創建對象。

在前面的例子中,我們不需要一個閉包。“工廠函數”本質上是一個類定義的奇怪的例子。在Java中,我們可以簡單地定義一個類,如下所示:

class MyJavaCounter {
    private int value;
    public MyJavaCounter(int initValue) { this.value = initValue; }
    public int increment() { return value++; }
    public int get() { return value; }
}
MyJavaCounter v = new MyJavaCounter(42);
System.out.println(v.get());      // returns 42
System.out.println(v.increment()); // returns 42
System.out.println(v.get());      // returns 43

修改自由變量是一個壞習慣

修改自由變量(即在lambda函數之外定義的任何對象)的Lambda函數可能會產生混淆。其他功能的副作用可能會導致不必要的錯誤。

這是典型的老年語言的開發人員不明白為什么JavaScript產生隨機的莫名其妙的行為。 在功能語言中,它通常是有限的,而當它不是,則不鼓勵。

考慮你正在使用并行范例,例如在Spark中:

int counter = 0;
JavaRDDrdd = sc.parallelize(data);
rdd.foreach(x -> counter += x); // Don't do this!!

結論

我們已經看到了一個非常簡要的Java 8 lambda表達式。我們專注于匿名類和lambda表達式之間的區別。之后,我們更好地看到了閉包的概念,看看它們是如何在JavaScript中實現的。此外,我們看到JavaScript的閉包不能直接在Java 8中使用,以及如何通過對象引用來模擬它。

我們還發現,當我們將它們與JavaScript之類的語言進行比較時,Java對閉包的支持有限。

然而,我們看到這些限制并不真正重要。事實上,閉包在JavaScript中被用作定義類和創建對象的基本機制,我們都知道它不是Java問題。

 

 

來自:https://blog.maxleap.cn/archives/1297

 

 本文由用戶 ijxx4750 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!