Java 8 習慣用語(10):使用閉包捕獲狀態

Java 8 習慣用語(10):使用閉包捕獲狀態

在 Java™ 編程中,咱們以不嚴格地使用術語 lambda 表達式來表示 lambda 表達式和閉包。但在某些狀況下,理解它們的區別很重要。lambda 表達式是無狀態的,而閉包是帶有狀態的。將 lambda 表達式替換爲閉包,是一種管理函數式程序中的狀態的好方法。html

無狀態的生活

咱們在本系列中大量介紹了 lambda 表達式,您應該已經對它們有很是透徹的瞭解。它們是小巧的匿名函數,接受可選的參數,執行某種計算或操做,並且可能返回一個結果。lambda 表達式也是無狀態的,這可能會在您的代碼中產生重大影響。java

咱們首先看一個使用 lambda 表達式的簡單示例。假設咱們想將一個數字集合中的偶數乘以二。一種選擇是使用 Stream 和 lambda 表達式建立一個函數管道,以下所示:編程

numbers.stream()
 .filter(e -> e % 2 == 0)
 .map(e -> e * 2)
 .collect(toList());

咱們傳入 filter 中的 lambda 表達式取代了 Predicate 函數接口。它接受一個數字,若是該數字是偶數,則返回 true,不然返回 false。另外一方面,咱們傳遞給 map 的 lambda 表達式取代了 Function 函數接口:它接受任何數字並返回該值的兩倍。閉包

這兩個 lambda 表達式都依賴於傳入的參數和字面常量。兩者都是獨立的,這意味着它們沒有任何外部依賴項。由於它們依賴於傳入的參數,並且可能還依賴於一些常量,因此 lambda 表達式是無狀態的。它們很可愛,也很安靜,就像熟睡的嬰兒同樣。ide

咱們爲何須要狀態

如今讓咱們更仔細地看看傳遞給 map 方法的 lambda 表達式。若是咱們但願計算給定值的三倍或四倍,該怎麼辦?咱們能夠將常量 2 轉換爲一個變量(好比 factor),但 lambda 表達式仍須要一種方式來獲取該變量。函數

咱們能夠推斷,lambda 表達式能夠採用與接收參數 e 的相同方式來接收 factor,以下所示:code

.map((e, factor) -> e * factor)

還不錯,但不幸的是它不起做用。方法 map 要求接受函數接口 Function<T, R> 的一個實現做爲參數。若是咱們傳入該接口外的任何內容(好比一個 BiFunction<T, U, R>),map 不會接受。須要採用另外一種方式將 factor 提供給咱們的 lambda 表達式。htm

詞法範圍

函數要求變量在限定範圍內。由於它們其實是匿名函數,因此 lambda 表達式也要求引用的變量在限定範圍內。一些變量以參數形式被函數或 lambda 表達式接收。一些變量是局部定義的。一些變量來自函數外部,位於所謂的詞法範圍中。對象

下面是一個詞法範圍示例。blog

public static void print() {
 String location = "World";
  
 Runnable runnable = new Runnable() {
 public void run() {
 System.out.println("Hello " + location);
 }
 };
  
 runnable.run();}

print 方法中,location 是一個局部變量。可是,Runnablerun 方法還引用了一個不是 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 上執行。由於 Runnablerun 方法是從 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();
 }}

在這個示例中,create 方法有一個局部變量 value,該變量的壽命很短:只要咱們退出 create,它就會消失。create 內建立的閉包在其詞法範圍中引用了這個變量。在完成 create 方法後,該方法將閉包返回給 main 中的調用方。在此過程當中,它從本身的堆棧中刪除變量 value,並且 lambda 表達式將會執行。這是結果輸出:

exiting createIn main4

咱們知道,在 main 中調用 run 時,create 中的 value 就會終止。儘管咱們能夠假設 lambda 表達式中的 value 直接被綁定到它的詞法範圍中的變量,但該假設並不成立。

可經過一個類比來揭示其中原委。

閉包午休時間

假設個人辦公室離家約 10 英里(使用改進的測量單位的話爲 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_41: istore_02: iload_03: 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  
原文連接: Java 8 習慣用語
原出處: IBM Developer

e3f6982e97de58e289858a0ec142affe.jpeg

相關文章
相關標籤/搜索