Java 8 Lambda限制:閉包

摘要: 主要討論一下java8 lambda的一些限制和閉包的概念,以及簡單比較一下java和javascripe實現閉包的區別。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表達式的實現之間存在差別。this

可是,主要的一點我想在此強調的是,考慮到他們在封閉範圍如何交互,咱們能夠認爲它們只是一種建立匿名類接口的緊湊方式,好比 RunnableCallableFunctionPredicate ,等。實際上,lambda表達式和它的封閉範圍之間的相互做用保持徹底相同(即this 關鍵字語義上的差別 )。spa

Java 8 Lambda限制

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

例如,考慮如下示例:

void fn() {
    int myVar = 42;
    Supplier<Integer> lambdaFun = () -> 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) {
    MyClosure closure = 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;
JavaRDD rdd = sc.parallelize(data);
rdd.foreach(x -> counter += x); // Don't do this!!

結論

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

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

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


做者信息
本文系力譜宿雲LeapCloud旗下MaxLeap團隊_Service&Infra成員:賈威威 【翻譯】
從過後端開發已有多年,目前主要負責MaxWon服務端部分功能的開發與設計。
原文連接:https://dzone.com/articles/ja...

相關文章
次時代Java編程(一):Java裏的協程

做者往期佳做
使用Vert.x構建Web服務器和消息系統
集中式內存緩存Guava Cache

歡迎關注微信公衆號
這裏寫圖片描述

相關文章
相關標籤/搜索