1. gzyueqian
      18529173453
      首頁 > 新聞中心 > > 正文

      Java 理論與實踐: 閉包之爭

      更新時間: 2007-05-31 10:16:25來源: 粵嵌教育瀏覽量:608


        提起向 Java™ 語言增加新的特性,每個人都有自己的一兩個想法。隨著 Java 平臺的源代碼日漸開放,而使用其他語言(例如 JavaScript 和 Ruby)作為服務器端應用程序日趨流行,因此關于 Java 語言未來的爭論空前激烈。Java 語言是否應該包容像閉包這樣的主流新特性,然而引入過多特性會不會使得這種好端端的語言過于龐雜?在這個月的 “ Java 理論與實踐 ” 專題中,Brian Goetz 回顧了相關的概念,詳細介紹了兩種競爭的閉包方案。

        在 跨越邊界 系列近的一篇文章中,我的朋友兼同事 Bruce Tate 以 Ruby 為例描述了閉包的強大功能。近在安特衛普召開的 JavaPolis 會議上,聽眾人數多的演講是 Neal Gafter 的 “向 Java 語言增加閉包特性”。在 JavaPolis 的公告欄上,與會者可以寫下和 Java 技術有關(或者無關)的想法,其中將近一半和關于閉包的爭論有關。近似乎 Java 社區的每個人都在討論閉包——雖然閉包這一業已成熟的概念早在 Java 語言出現的 20 年之前就已經存在了。

        本文中,我的目標是介紹關于 Java 語言閉包特性的種種觀點。本文首先介紹閉包的概念及其應用,然后簡要說明目前提出來的相互競爭的一些方案。

        閉包:基本概念

        閉包是可以包含自由(未綁定)變量的代碼塊;這些變量不是在這個代碼塊或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義。“閉包” 一詞來源于以下兩者的結合:要執行的代碼塊(由于自由變量的存在,相關變量引用沒有釋放)和為自由變量提供綁定的計算環境(作用域)。在 Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby 和 Python 等語言中都能找到對閉包不同程度的支持。

        閉包的價值在于可以作為函數對象 或者匿名函數,對于類型系統而言這就意味著不僅要表示數據還要表示代碼。支持閉包的多數語言都將函數作為級對象,就是說這些函數可以存儲到變量中、作為參數傳遞給其他函數,重要的是能夠被函數動態地創建和返回。比如下面清單 1 所示的 Scheme 例子(摘自 SICP 3.3.3):

      清單 1. Scheme 編程語言的函數示例,該函數接受另一個函數作為參數并返回緩存后的函數

      (define (memoize f)
      (let ((table (make-table)))
      (lambda (x)
      (let ((previously-computed-result (lookup x table)))
      (if (not (null? previously-computed-result))
      previously-computed-result
      (let ((result (f x)))
      (insert! x result table)
      result))))))

        上述代碼定義了一個叫做 memoize 的函數,接受函數 f 作為其參數,返回和 f 計算結果相同的另一個函數,不過新函數將以前的計算結果保存在表中,這樣讀取結果更快。返回的函數使用 lambda 結構創建,該結構動態創建新的函數對象。斜體顯示的標識符在新定義函數中是自由的,它們的值在創建該函數的環境中綁定。比如,用于存儲緩存數據的表變量在調用 memoize 的時候創建,由于被新建的函數引用,因此直到垃圾回收器回收結果函數的時候才會被收回。如果調用結果函數時帶有參數 x ,它首先檢查是否已經計算過 f(x)。是的話返回已經得到的 f(x),否則計算 f(x) 并在返回之前保存到表中以備后用。

        閉包為創建和操縱參數化的計算提供了一種緊湊、自然的方式。可以認為支持閉包就是提供將 “代碼塊” 作為級對象處理的能力:能夠傳遞、調用和動態創建新的代碼塊。要完全支持閉包,這種語言必須支持在運行時操縱、調用和創建函數,還要支持函數可以捕獲創建這些函數的環境。很多語言僅提供了這些特性的一個子集,具備閉包的部分但不是全部優勢。關于是否要在 Java 語言中增加閉包,關鍵問題在于提高表達能力所帶來的益處能否與更高的復雜性所帶來的代價相抵消。

        匿名類和函數指針

        C 語言提供了函數指針,允許將函數作為參數傳遞給其他函數。但是,C 中的函數不能有自由變量:所有變量在編譯時必須是已知的,這就降低了函數指針作為一種抽象機制的表達能力。

        Java 語言提供了內部類,可以包含對封閉對象字段的引用。該特性比函數指針更強大,因為它允許內部類實例保持對創建它的環境的引用。乍看起來,內部類似乎確實提供了閉包的大部分作用,雖然這還不是全部作用。您可以很容易構造一個名為 UnaryFunction 的接口,并創建能夠緩存任何 unary 函數的緩存包裝程序。但是這種方法通常不易于實現,它要求與函數交互的所有代碼在編寫時都必須知道這個函數的 “框架”。

        閉包作為一種模式模板

        匿名類允許創建這樣的對象,該對象能夠捕獲定義它們的一部分環境,但是對象和代碼塊不一樣。以一個常見的編碼模式為例,如執行帶有 Lock 的代碼塊。如果需要遞增帶有 Lock 的計數器,代碼如清單 2 所示——即使這么簡單的操作也非常羅嗦:

      清單 2. 執行加鎖代碼塊的規范用法

      lock.lock();
      try {
      ++counter;
      }
      finally {
      lock.unlock();
      }

        如果能夠提取出加鎖管理代碼就好了,這樣會使代碼看起來更緊湊,也不容易出錯。首先可以創建如清單 3 所示的 withLock() 方法:
      清單 3. 提取了 “加鎖執行” 的概念,但是問題在于缺乏異常的透明性

      public static void withLock(Lock lock, Runnable r) {
      lock.lock();
      try {
      r.run();
      }
      finally {
      lock.unlock();
      }
      }

        不幸的是,這種方法只能達到您預期的部分目標。創建這種抽象代碼的目標之一是使代碼更緊湊;但是,匿名內部類的語法不是很緊湊,調用代碼看起來如清單 4 所示:

      清單 4. 清單 3 中 withLock() 方法的客戶端代碼

      withLock(lock,
      new Runnable() {
      public void run() {
      ++counter;
      }
      });

        要遞增一個加鎖的計數器仍然需要編寫很多代碼!另外,將受到鎖保護的代碼塊轉化成方法調用所帶來的抽象問題大大增加了問題的復雜性——如果受保護的代碼塊拋出一個檢測異常怎么辦?現在我們不能使用 Runnable 來表示執行的任務,而必須創建一種新的表示方法以允許在方法調用中拋出異常。不幸的是,在這里泛化也幫不上多少忙,雖然方法可以用泛型參數 E, 表示可能拋出的檢測異常,但是這種方法不能很好地泛化拋出多種檢測異常類型的方法(這就是為何 Callable 中的 call() 方法聲明為拋出 Exception 而不是用類型參數指定一個類型的原因)。清單 3 中的方法的問題在于缺乏異常透明性,除此之外,還存在其他非透明性的問題,在 清單 4 的 Runnable 上下文中,return 或 break 這類語句的含義,與 清單 2 中 try 語句塊中的一般意義不同。

        理想情況下,受保護的遞增操作應該像清單 5 所示的那樣,并且塊中代碼的含義和 清單 2 的擴展形式相同:

      清單 5. 清單 3 客戶端代碼的理想形式(但是是假設形式)

      withLock(lock,
      { ++counter; });

        在語言中添加閉包以后,就可以創建行為類似控制流結構的方法,比如 “加鎖執行這段代碼”、“操作流并在完成后將其關閉” 或者 “為代碼塊的執行計時” 等。這種策略有可能簡化某些類型的代碼,這些代碼反復使用特定編碼模式或者慣用法,比如 清單 2 所示的加鎖用法。(在一定程度上提供類似表達能力的另一種技術是 C 預處理器,它可以將 withLock() 操作用預處理宏表示,雖然和閉包相比宏更難以組織,而且安全性也更差。)

        泛化算法的閉包

        閉包能夠大大簡化代碼的另一個地方是泛化算法的使用。隨著多處理器計算機越來越便宜,利用小粒度并行機制的重要性日漸突出。使用泛化算法定義計算為庫實現在問題空間中采用并行機制提供了一種自然的方式。

        比方說,假設要計算一個大型數字集合的平方和。清單 6 給出了一種計算方法,但這種方法是按順序計算結果的,對于大規模多處理器系統可能不是效率的方法:

      清單 6. 順序計算平方和

      double sum;
      for (Double d : myBigCollection)
      sum += d*d;

        每次循環迭代有兩個操作:取平方,累加到終結果。平方操作是互相獨立的,可以并行執行;加法操作也不一定要執行 N 次,如果計算組織得當,只要 log(N) 次操作即可完成。

        清單 6 中的操作是 map-reduce 算法的一個示例,對大批數據元素中的每一個數據元素應用一個函數,然后將每次應用該函數計算出的結果通過某種累加函數累加起來。假設有一個 map-reduce 實現過程接受數據集作為輸入,用一元函數處理每個元素,用二元函數累加結果,則可用清單 7 所示的代碼完成平方和運算:

      清單 7. 使用 MapReduce 計算平方和,可以實現并行執行

      Double sumOfSquares = mapReduce(myBigCollection,
      new UnaryFunction<Double> {
      public Double apply(Double x) {
      return x * x;
      }
      },
      new BinaryFunction<Double, Double> {
      public Double apply(Double x, Double y) {
      return x + y;
      }
      });

        假設清單 7 中的 mapReduce() 實現知道哪些操作可以并行執行,因而可以將函數應用和累加過程并行執行,從而改進并行系統的吞吐量。但是清單 7 中的代碼不簡潔,用了更多代碼來表達和清單 6 中三行代碼等價的泛化算法。

        通過閉包可以更好地管理清單 7 中的代碼。比如,清單 8 中的閉包語法和目前提出的 Java 語言閉包方案都不一樣,目的僅在于說明閉包對泛化算法的支持:

      清單 8. 使用 MapReduce 和假設的閉包語法計算平方和

      sumOfSquares = mapReduce(myBigCollection,
      function(x) {x * x},
      function(x, y) {x + y});

        清單 8 中基于閉包的算法具有兩方面的好處:代碼容易閱讀和編寫,抽象層次比順序循環更高,能夠有效地通過庫實現并行。

        閉包方案

        目前至少提出了兩種向 Java 語言增加閉包的方案。其一,綽號為 “BGGA”(名字源于其作者 Gilad Bracha、Neal Gafter、James Gosling 和 Peter von der Ahe),它擴展了類型系統,引入了 function 類型。其二,綽號為 “CICE” (代表 Concise Inner Class Expressions,簡潔內部類表示),是由 Joshua Bloch、Doug Lea 和 “瘋狂的” Bob Lee 所支持的,其目標更謙虛:簡化匿名內部類實例的創建。 JSR 可能很快就會收到這方面的提議,考慮在未來的 Java 語言版本中支持閉包的形式和程度。

        BGGA 方案

        BGGA 方案提出了 function 類型的概念,即函數都帶有一個類型參數列表、返回類型和 throws 子句。在 BGGA 方案中,計算平方和的代碼將如清單 9 所示:

      清單 9. 使用 BGGA 閉包語法計算平方和

      sumOfSquares = mapReduce(myBigCollection,
      { Double x => x * x },
      { Double x, Double y => x + y });

        => 字符到左側花括號之間的代碼表示參數的名稱和類型,右側的代碼表示定義的匿名函數的實現。這段代碼可以引用塊中定義的局部變量、閉包的參數以及創建閉包的作用域中的變量。

        在 BGGA 方案中,可以聲明 function 類型的變量、方法參數和方法返回值。在需要一個抽象方法類(如 Runnable 或 Callable)實例的任何上下文中都可以使用閉包,對于匿名類型的閉包,您可以使用帶有給定參數列表的 invoke() 方法來調用。

        BGGA 方案的主要目標之一是允許程序員創建行為類似控制結構的方法。因此,BGGA 還在語法上提出了一些吸引人的花招,允許像新的關鍵字那樣調用接受閉包的方法,從而能夠創建像 withLock() 或 forEach() 這樣的方法,然后向控制原語一樣調用它們。清單 10 說明了根據 BGGA 方案如何定義 withLock() 方法,清單 11 和 清單 12 說明了如何調用該方法,包括標準形式和“控制結構”形式:

      清單 10. 采用 BGGA 閉包方案編寫的 withLock() 方法

      public static <T,throws E extends Exception>
      T withLock(Lock lock, {=>T throws E} block) throws E {
      lock.lock();
      try {
      return block.invoke();
      } finally {
      lock.unlock();
      }
      }

        清單 10 中的 withLock() 方法接受鎖和閉包。閉包的返回類型和 throws 子句是泛化參數,編譯器中的類型推斷通常允許在未指定 T 和 E 值的情況下調用,如清單 11 和 12 所示:

      清單 11. 調用 withLock()

      withLock(lock, {=>
      System.out.println("hello");
      });

      清單 12. 使用控制結構的縮寫形式調用 withLock()

      withLock(lock) {
      System.out.println("hello");
      }

        和泛化一樣,BGGA 方案中閉包的復雜性在很大程度上是由庫的編寫者來分擔的,使用接受閉包的庫方法更簡單。

        使用內部類實例是閉包所帶來的好處,但是這種方法缺少透明性,BGGA 方案在一定程度上還有助于解決這個問題。比如,return、 break 和 this 在某一代碼塊中的語義與其在 Runnable(或其他內部類實例)中同一代碼塊中的語義是不同的。為了利用泛化算法而對代碼進行移值的時候,這些不透明因素可能會造成混亂。

        CICE 方案

        CICE 方案要簡單得多,它解決了實例化內部類實例不太靈活的問題。它沒有建立函數類型的概念,只不過為一個抽象方法(如 Runnable、Callable 或 Comparator)內部類實例化提出了一種更緊湊的語法。

        清單 13 說明了按照 CICE 如何計算平方和。它顯示使用了 mapReduce() 中的 UnaryFunction 和 BinaryFunction 類型。mapReduce() 的參數是從 UnaryFunction 和 BinaryFunction 派生的匿名類,這種語法大大了降低了創建匿名實例的冗余。

      清單 13. 采用 CICE 閉包方案計算平方和的代碼

      Double sumOfSquares = mapReduce(myBigCollection,
      UnaryFunction<Double>(Double x) { return x*x; },
      BinaryFunction<Double, Double>(Double x, Double y) { return x+y; });

        由于為傳遞給 mapReduce() 的函數所創建的對象是普通的匿名類實例,其函數體可以引用封閉域中定義的變量,清單 13 中的方法和清單 7 相比,的區別在于語法的繁簡程度。

      結束語

        BGGA 方案為 Java 這種語言增加了功能強大的新武器,但是同時也為其語義和語法帶來了可以預見的復雜性。另一方面,CICE 方案更簡單:利用語言中已有的特性并使其更易于使用,但是沒有增加重要的新功能。閉包是一種強大的抽象機制,用過之后多數人不愿意放棄。(問問那些熟悉 Scheme、Smalltalk 或 Ruby 編程的朋友對閉包的感想如何,他們可能會反問您對呼吸有什么感想。)但語言是有機的整體,為語言增加初設計時沒有預料到的新特性充滿了危險,而且會增加語言的復雜性。爭論的焦點不在于閉包是否有用——因為答案顯然是肯定的——而在于為閉包重新改造 Java 語言的好處是否抵得上要付出的代價。

      免費預約試聽課

      亚洲另类欧美综合久久图片区_亚洲中文字幕日产无码2020_欧美日本一区二区三区桃色视频_亚洲AⅤ天堂一区二区三区

      
      

      1. 亚洲理论在线中文字幕a | 性色AⅤ在线播放 | 在线人成视频播放午夜福 | 亚洲日韩日本一区二区 | 熟女一区二区不卡 | 亚洲精品在线免费 |