Java 8 Lambda限制:閉包
假設我們想創建一個簡單的線程,只在控制臺上打印一些東西:
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