Java閉包

聲明

本文會大致講解下java 閉包(請允許我這麼稱呼Closure,因爲javascript好多書都是這麼翻譯的),本文的內容是從Understanding the closures debate摘抄翻譯出來的,如果你英文夠好並且耐心充足,推薦你去閱讀原文,以便獲得更好的上下文語境。

什麼是閉包

本質上,閉包是一段可以被傳遞進函數執行的代碼。某種程度上,他和java內部類的概念相似。但是閉包包含更多內容。

爲了更好的理解java閉包的概念,可以考慮如下情景,你想傳遞一段代碼到一個forEach循環函數去執行:一個forEach函數循環地對每個forEach中的對象執行你傳入的代碼。你會如何去做,目前來說你會定義一個接口,我們就叫Block,之後呢將該接口的實現傳遞進forEach方法中,就像下面的例子所作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Block<T> {
  void invoke(T arg);
}
 
public class Utils {
  public static <T> void forEach(Iterable<T> seq, Block<T> fct) {
    for (T elm : seq)
    fct.invoke(elm);
  }
}
 
public class Test {
  public static void main(String[] args) {
    List<Integer> nums = Arrays.asList(1,2,3);
    Block<Integer> print = new Block<Integer>() {
      public void invoke(Integer arg) {
        System.out.println(arg);
      }
    };
    Utils.forEach(nums,print);
  }
}

就目前要把一段函數傳進另一個方法去執行,我們不可避免的會使用接口,然後將接口的實現傳進相應的方法中去立刻或者延遲,同步或者異步執行。閉包將會通過更加精煉的語法簡化這一過程,從而移除一些java的冗餘。除了帶來更加精煉和可讀的語法,閉包還會將會帶來java全新的功能,比如定製化的控制結構。

閉包vs內部類

在近些年的發佈的blog中,java之父James Gosling討論了java閉包的歷史

Closures were left out of Java initially more because of time pressures than anything else. In the early days of Java the lack of closures was pretty painful, and so inner classes were born: an uncomfortable compromise that attempted to avoid a number of hard issues. But as is normal in so many design issues, the simplifications didn't really solve any problems, they just moved them.

"最初閉包沒有包括在java語言當中主要是因爲時間緊迫。早些年java語言對於缺少閉包功能相當痛苦,所以內部類就誕生了:一個試圖解決一些難題的不舒服的承諾。儘管內部類的應用已經十分正常,但是內部類這種簡化方式並沒有解決任何問題,只是簡單移走了問題。

Gosling十分準確地描述了我們當下的問題:從JDK1.1版本誕生的11年來,我們一直用內部類來代替閉包實現功能。他們的確幫助我們很多,但是內部類並沒有真正代替閉包。內部類只是沒有他們該有的強大和簡潔。

三個閉包提議(BGGA, FCM, CiCE)對比

按照複雜度排序 BGGA->FCM(First Class MEthods) plus JCA(Java control abstraction)->CiCE(Concise Instance Creation Expression) plus ARM(Automatic Resource Management)

閉包提議實踐

所有的這些閉包提議都致力於簡化將一段功能傳遞進方法的過程。讓我們看看不同的提議是如何實現的

CICE (Concise Instance Creation Expression)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Block<T> {
  void invoke(T arg);
}
public class Utils {
  public static <T> void forEach(Iterable<T> seq, Block<T> fct) {
    for (T elm : seq)
    fct.invoke(elm);
  }
}
public class Test {
  public static void main(String[] args) {
    List<Integer> nums = Arrays.asList(1,2,3);
    Block<Integer> print
      = Block<Integer>(Integer arg) { System.out.println(arg); };
    Utils.forEach(nums,print);
  }
}

我們創建了一個閉包而不是一個內部類。本質上這個提議最終演化成一個更加簡潔的語法,因爲我們可以用閉包這個更加簡潔的語法替換掉內部類能實現的功能。閉包的語法適用於所有隻有一個方法的接口,比如Runnable, Callbable...CICE實現了簡化這些接口的使用。

BGGA

此提議向Java語言引入了一個全新的類型,即函數類型(function types)。一個函數類型標識閉包。通過函數類型我們不再需要Block這個接口了(在我們的例子中), 而是使用函數類型{T => void},這個標記的意思是一個返回值爲void的函數接受一個類型爲T的參數。

1
2
3
4
5
6
class Utils {
  public static <T> void forEach(Iterable<T> seq, {T => void} fct) {
    for (T elm : seq)
    fct.invoke(elm);
  }
}

所有閉包都隱式地含有一個invoke方法。本質上你可以理解爲編譯器底層將這個函數類型合成爲一個和Block一樣的接口。

我們用一個比較相似的函數類型{Integer => void}創建一個閉包,並將其傳入forEach方法中。

1
2
3
4
5
6
7
8
class Test {
  public static void main(String[] args) {
    List<Integer> nums = Arrays.asList(1,2,3);
    {Integer => void } print
       = { Integer arg => System.out.println(arg); };
    Utils.forEach(nums,print);
  }
}

變量print指向一個函數類型(即閉包)。創建的閉包接入一個Integer類型的參數,並將其打印出來。接着將這個閉包傳遞進forEach方法中打印每一個nums數組中的元素。

FCM

此提議更進一步並沒有加入函數類型(function types)來標識閉包的類型,而是標識總的方法類型。在此提議下,閉包僅僅是一個方法的特例,即匿名內部方法(一個沒有名字的方法)。當然,這種語法也是不同的。這種提議下函數類型用#(void(T))標識,閉包則用#(Integer arg) {System.out.println(arg); }標識

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Utils {
  public static <T> void forEach(Iterable<T> seq, #(void(T)) fct) {
    for (T elm : seq)
    fct.invoke(elm);
  }
}
class Test {
  public static void main(String[] args) {
    List<Integer> nums = Arrays.asList(1,2,3);
    #(void(Integer)) print
      = #(Integer arg) { System.out.println(arg); };
    Utils.forEach(nums,print);
  }
}

從這個例子可以看出,這種提議用不一樣的語法來實現其功能(通過將一段功能傳遞進一個方法中),但是底層的理念是相似的。下面我們將比較下不同方式如何實現閉包轉換和類型兼容的。

閉包轉換(Closure conversion)

這三總提議都允許一定程度上的閉包和接口間的類型兼容:一個閉包可以傳遞進一個兼容類型的接口類型上。這是一個十分重要的屬性,因爲它實現了閉包對當下內部類的替換可能。這種屬性提供了對接口(只有一個方法定義,包括繼承)和內部類的向後兼容性。

閉包轉換的例子

考慮下一個用Runable接口實現的閉包轉換,我們將一個閉包傳遞給一個類型爲Runnable接口的變量。在當前的Java語言中我們會用內部類來實現快速的(on-the-fly)Runnable接口實現。

1
2
Runnable r = new Runnable() { public void run() { System.out.println("Hello World."); } };
new Thread(r).start();

CICE下,可以通過一個更加簡潔的語法實現同樣的事。所以我們對CICE下閉包對接口的類型兼容並不感到驚訝。

1
2
Runnable r = Runnable() { System.out.println("Hello World."); };
new Thread(r).start();

在BGGA 和FCM下,閉包和接口的類型轉換就沒那麼明顯了。

1
2
3
4
5
6
7
8
9
//BGGA
Runnable r = { => System.out.println("Hello World."); };
new Thread(r).start();
 
 
```java
//FCM
Runnable r = # { System.out.println("Hello World."); };
new Thread(r).start();

簡潔,便利的語法標註是三種提議都努力爭取的目標。

非本地控制聲明(Non-local control statements)

儘管在很多方面三個提議是十分相似的,但是在一個方面三個提議分歧卻很大。 BGGA下,它允許閉包的控制語句影響閉包所定義的上下文環境(context),而不僅僅是閉包內部實現的環境。下面代碼展示了一個non-local return 在forEach循環的應用。這個閉包會打印(print)它每接到的一個參數,當接到的參數是3時,會立即中斷main方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//BGGA notation
class Utils {
  public static <T> void for<each(Iterable<T> seq, {T => void } fct) {
    for (T elm : seq)
    fct.invoke(elm);
  }
}
 
class Test {
  public static void main(String[] args) {
    List<Integer> nums = Arrays.asList(1,2,3,4,5);
    {Integer => void } print = { Integer arg ==> if (arg == 3) return; System.out.println(arg); };
    Utils.forEach(nums,print);
  }
}

上面代碼將在閉包接收到3參數時,中斷閉包和閉包定義所在的外部的main方法的執行流程。

非本地控制聲明只有BGGA提供了實現,CICE和FCM下return等控制語句只是中斷閉包內部的執行流程,並不會影響閉包所定義位置的外部環境。

BGGA需要非本地return聲明因爲它的目標就是要通過閉包去掉常用的控制結構,我們會在下面進一步描述。這後面的想法是閉包的行爲在它定義的外部環境中和一段代碼一樣。就是閉包可以獲取它外部(enclosing scope)的變量。

語義綁定(Lexical binding)

BGGA應用如下的語義綁定

  • 包含域中的變量
  • this
  • break, continue, return

閉包中的語義綁定意味着閉包可以訪問閉包外部域的變量。

this關鍵字的語義綁定是java語言帶來的新的特性之一。然而在覓名類中,this卻指向內部類中的屬性值,而不是外部類的屬性。在閉包中,CICE使用和傳統內部類相似的做法,this指向閉包中的變量值,而BGGA和FCM使用this指向閉包外部域的變量。

三個提議最基本的分歧就是控制語句的語義綁定。BGGA中,不光是return語句,break,continue都被語義綁定了。只有BGGA提議這種特性。CICE和FCM都沒有。

BGGA需要控制語句的語義綁定來移除掉普遍的控制代碼。反觀,FCM提議主要強調的是對函數式編程提供良好的支持,而不是移除普遍的控制代碼。而FCM提案只有this關鍵字的語義綁定,break, continue, return關鍵字並沒有,FCM這樣做基於很好的理由-非本地控制語句聲明有它的陷阱,一會我們將看到。

BGGA閉包下返回一個值(Returning a value from a BGGA closure)

原鏈接:http://daywasted.diandian.com/post/2013-03-07/40048655899

英文鏈接:http://www.javaworld.com/article/2077869/scripting-jvm-languages/understanding-the-closures-debate.html

發佈了20 篇原創文章 · 獲贊 1 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章