在 Java 編程中,咱們以不嚴格的術語 lambda 表達式來表示 lambda 表達式和閉包。可是在某些狀況下,理解它們的區別很重要。lambda 表達式是無狀態的,而閉包是帶有狀態的。將 lambda 表達式替換爲閉包,是一種管理函數式程序中的狀態的好方法。java
咱們在這個系列中介紹了 lambda 表達式,您應該已經對他們很是的瞭解了。它們是小巧的匿名函數,接受可選的參數,執行某種計算或操做,並且可能返回一個結果。lambda 表達式也是無狀態的,這可能會在您的代碼中產生重大影響。編程
咱們首先來看一個使用 lambda 表達式的簡單示例。假設咱們想將一個數字集合中的偶數乘以二。一種是使用 Stream 和 lambda 表達式建立一個函數管道,入下所示:bash
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.collect(toList());
複製代碼
咱們傳入 filter 中的 lambda 表達式取代了 Predicate 函數接口。它接收一個數字,若是該數字是偶數,則返回 true,不然返回 false。另外一方面,咱們傳遞給 map 的 lambda 表達式取代了 Function 函數接口:它接受任何數字並返回該值的兩倍。這個lambda 表達式都依賴傳入的參數和字面常量。兩者都是獨立的,這意味着他們沒有任何外部依賴項。由於它們依賴於傳入的參數,並且可能還依賴於一些常量,因此 lambda 表達式是無狀態的。閉包
如今讓咱們更仔細地看看傳遞給 map 方法的 lambda 表達式。若是咱們但願計算給定值的三倍或四倍,該怎麼辦?咱們能夠將常量 2 轉換爲一個變量(好比 factor),但 lambda 表達式仍須要一種方式來獲取該變量。函數
咱們能夠推斷,lambda 表達式能夠採用與接收參數 e 的相同方式來接收 factor,以下所示:測試
.map((e, factor) -> e * factor)
複製代碼
還不錯,但不幸的是它不起做用。方法 map 要求接受函數接口 Function<T, R>
的一個實現做爲參數。若是咱們傳入該接口外的任何內容(好比一個 BiFunction<T, U, R>
),map 不會接受。須要採用另外一種方式將 factor 提供給咱們的 lambda 表達式。spa
函數要求變量在限定範圍內。由於它們其實是匿名函數,因此 lambda 表達式也要求引用的變量在限定範圍內。一些變量以參數形式被函數或 lambda 表達式接收。一些變量是局部定義的。一些變量來自函數外部,位於所謂的詞法範圍中。code
詞法範圍示例:cdn
public static void print() {
String location = "World";
Runnable runnable = new Runnable() {
public void run() {
System.out.println("Hello " + location);
}
};
runnable.run();
}
複製代碼
在 print 方法中,location 是一個局部變量。可是,Runnable 的 run 方法還引用了一個不是 run 方法的局部變量或參數的 location。對 Hello 旁邊的 location 的引用被綁定到 print 方法的 location 變量。對象
詞法範圍是函數的定義範圍。反過來,它也多是該定義範圍的定義範圍,等等。
在前面的代碼中,方法 run 沒有定義 location 或接收它做爲參數。run 的定義範圍是 Runnable 的匿名內部對象。由於沒有將 location 定義爲該實例中的字段,因此會繼續搜索匿名內部對象的定義範圍 — 在本例中爲方法 print 的局部範圍。
若是 location 不在該範圍中,編譯器會繼續在 print 的定義範圍內搜索,直到找到該變量或搜索失敗。
lambda表達式中的詞法範圍
咱們使用 lambda 表達式重寫前面的代碼:
public static void print() {
String location = "World";
Runnable runnable = () -> System.out.println("Hello " + location);
runnable.run();
}
複製代碼
得益於 lambda 表達式,代碼變得更簡潔,但 location 的範圍和綁定沒有更改。lambda 表達式中的變量 location 被綁定到 lambda 表達式的詞法範圍中的變量 location。嚴格來說,此代碼中的 lambda 表達式是一個閉包。
Lambda 表達式不依賴於任何外部實體;它們是依賴於自身參數和常量的內容。另外一方面,閉包既依賴於參數和常量,也依賴於它們的詞法範圍中的變量。從邏輯上講,閉包被綁定到它們的詞法範圍中的變量。可是,儘管邏輯上講是這樣,但實際上並不老是這麼作。有時甚至沒法執行這樣的綁定。兩個場景能夠證實這一點。
下面這段代碼將一個 lambda 表達式或閉包傳遞給一個 call 方法:
class Sample {
public static void call(Runnable runnable) {
System.out.println("calling runnable");
//level 2 of stack
runnable.run();
}
public static void main(String[] args) {
int value = 4; //level 1 of stack
call(
() -> System.out.println(value) //level 3 of stack
);
}
}
複製代碼
此代碼中的閉包使用了來自它的詞法範圍的變量 value。若是 main 是在堆棧級別 1 上執行的,那麼 call 方法的主體會在堆棧級別 2 上執行。由於 Runnable 的 run 方法是從 call 內調用的,因此該閉包的主體會在級別 3 上運行。若是 call 方法要將該閉包傳遞給另外一個方法(進而推遲調用的位置),則執行的堆棧級別可能高於 3。
您如今可能想知道在一個堆棧級別中的執行究竟如何能獲取以前的另外一個堆棧級別中的變量 — 尤爲是未在調用中傳遞上下文時。簡單來說就是沒法獲取該變量。
看另一個示例:
class Sample {
public static Runnable create() {
int value = 4;
Runnable runnable = () -> System.out.println(value);
System.out.println("exiting create");
return runnable;
}
public static void main(String[] args) {
Runnable runnable = create();
System.out.println("In main");
runnable.run();
}
}
複製代碼
測試結果:
exiting create
In main
4
複製代碼
在這個示例中,create 方法有一個局部變量 value,該變量的壽命很短:只要咱們退出 create,它就會消失。create 內建立的閉包在其詞法範圍中引用了這個變量。在完成 create 方法後,該方法將閉包返回給 main 中的調用方。在此過程當中,它從本身的堆棧中刪除變量 value,並且 lambda 表達式將會執行。
咱們知道,在 main 中調用 run 時,create 中的 value 就會終止。儘管咱們能夠假設 lambda 表達式中的 value 直接被綁定到它的詞法範圍中的變量,但該假設並不成立。
閉包午休時間
假設個人辦公室離家約 16 公,並且我早上 8 點出門上班。中午,我有短暫的時間用午飯,但出於健康考慮,我喜歡吃家裏烹飪的飯菜。因爲休息時間很短,只有在離家時帶上午飯,我才能吃上家裏的飯菜。
這就是閉包要完成的任務:它們攜帶本身的午飯(或狀態)。
讓咱們再看看 create 中的 lambda 表達式:
Runnable runnable = () -> System.out.println(value);
複製代碼
咱們編寫的 lambda 表達式沒有接受任何參數,但須要它的 value。編譯類 Sample 並運行 javap -c -p Sample.class 來檢查字節碼。您會注意到,編譯器爲該閉包建立了一個方法,該方法接受一個 int 參數:
private static void lambda$create$0(int);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_0
4: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
7: return
}
複製代碼
如今看看爲 create 方法生成的字節碼:
0: iconst_4
1: istore_0
2: iload_0
3: invokedynamic #2, 0 // InvokeDynamic #0:run:(I)Ljava/lang/Runnable;
複製代碼
值 4 存儲在一個變量中,而後,該變量被加載並傳遞到爲閉包建立的函數。在本例中,閉包保留着 value 的一個副本。這就是閉包攜帶狀態的方式。
如今,咱們回頭看看本文開頭的示例。除了計算集合中的偶數值的兩倍,若是咱們想要計算它們的三倍或四倍,該怎麼辦?爲此,咱們能夠將原始 lambda 表達式轉換爲一個閉包。
這是咱們以前看到的無狀態代碼:
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.collect(toList());
複製代碼
使用閉包而不是 lambda 表達式,代碼就會變成:
int factor = 3;
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * factor)
.collect(toList());
複製代碼
map 方法如今接受一個閉包,而不是一個 lambda 表達式。咱們知道,這個閉包接受一個參數 e,但它也捕獲並攜帶 factor 變量的狀態。
此變量位於該閉包的詞法範圍中。它能夠是定義 lambda 表達式的函數中的局部變量;能夠做爲該外部函數的一個參數傳入;能夠位於閉包的定義範圍(或該定義範圍的定義範圍等)中的任何地方。不管如何,該閉包將狀態從定義該閉包的代碼傳遞到了須要該變量的執行點。
閉包不一樣於 lambda 表達式,由於它們依賴於本身的詞法範圍來獲取一些變量。所以,閉包能夠捕獲並攜帶狀態。lambda 表達式是無狀態的,閉包是有狀態的。能夠在您的程序中使用閉包,將狀態從定義上下文攜帶到執行點。
感謝 Venkat Subramaniam 博士
Venkat Subramaniam 博士站點:http://agiledeveloper.com/
知識改變命運,努力改變生活