Java 8為什么需要Lambda表達式

jopen 11年前發布 | 71K 次閱讀 Lambda Java開發

    函數編程在C#、Python、JavaScript中都得到充分體現。而Java直到最新的Java 8才開始正式支持函數編程,最明顯的改進就是對Lamba表達式的支持。正如C#之父Anders Hejlsberg在那篇文章 編程語言大趨勢 中所講,未來的編程語言將逐漸融合各自的特性,而不存在單純的聲明式語言(如之前的Java)或者函數編程語言。將來聲明式編程語言借鑒函數編程思想,函數編程語言融合聲明式編程特性...這幾乎是一種必然趨勢。如下圖所示:

                                      Java 8為什么需要Lambda表達式

                                                影響力較大的三個趨勢

    那具體而言我們為什么需要Lambda表達式呢?難道Java的OO和命令式編程(imperative programming)特性不夠強大嗎?下面讓我們來分析下其原因。


1、內部循環和外部循環

     先看一個大家耳熟能詳的例子:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

for (int number : numbers) { System.out.println(number); }</pre>    是不是很常見呢?這個叫外部循環(External Iteration)。但是外循環有什么問題呢?簡單來說存在下面三個缺點:

  1. 只能順序處理List中的元素(process one by one)
  2. 不能充分利用多核CPU
  3. 不利于編譯器優化
  4. </ol>

        而如果利用內部循環,代碼寫成下面這樣:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    numbers.forEach((Integer value) -> System.out.println(value));</pre>    這樣就能規避上面的三個問題:

    1. 不一定需要順序處理List中的元素,順序可以不確定
    2. 可以并行處理,充分利用多核CPU的優勢
    3. 有利于JIT編譯器對代碼進行優化
    4. </ol>

          類似的C#從4.0版本開始也支持集合元素并行處理,代碼如下:

      List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
      Parallel.ForEach(nums, (value) =>
      {
         Console.WriteLine(value);
      });


      2、傳遞行為,而不僅僅是傳值

           如果你使用C#有一段時間的話,那么你很可能已經明白這個標題的意思了。在C#中,經常看到一些函數的參數是Action或者Func類型,比如下面這個:

      public class ArticleDac {
         ...
         public Article GetArticles(Func<IDbSet<Article>, Article> func)   // 這里傳遞的就是行為
         {
            using(var db = xx) {
               return func(db.Articles);
            }  
         }
         ...
      }
      // 下面是調用
      int articleId = 119;
      var firstArticle = new ArticleDac().GetArticles(
          articleDbSet =>
          articleDbSet.AsQueryable().FirstOrDefault(x => x.id == articleId)
      );
           看不懂?沒關系。我們先來看一個體現傳值局限性的場景吧,上代碼:
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

      public int sumAll(List<Integer> numbers) { int total = 0; for (int number : numbers) { total += number; } return total; }</pre>     sumAll算法很簡單,完成的是將List中所有元素相加。某一天如果我們需要增加一個對List中所有偶數求和的方法sumAllEven,如下:

      public int sumAllEven(List<Integer> numbers) {
          int total = 0;
          for (int number : numbers) {
              if (number % 2 == 0) {
                  total += number;
              }
          }
          return total;
      }
           又有一天,我們需要增加第三個方法:對List中所有大于3的元素求和,那是不是繼續加下面的方法呢?
      public int sumAllEven(List<Integer> numbers) {
          int total = 0;
          for (int number : numbers) {
              if (number > 3) {
                  total += number;
              }
          }
          return total;
      }

           比較這三個方法,我們發現了一個很明顯的“代碼臭味”—— 代碼重復(詳情參考《重構》),三個方法的唯一區別之處在于if判斷這一行代碼。如果脫離這里的上下文,我們會怎么做呢?我首先會先想到利用策略模式重構代碼如下:

      public interface Strategy {
         public boolean test(int num);
      }

      public class SumAllStrategy { public boolean test(int num) { return true; } }

      public class SumAllEvenStrategy { public boolean test(int num) { return num % 2 == 0; } }

      public class ContextClass { private Strategy stragegy = null; private final static Strategy DEFAULT_STRATEGY = new SumAllStrategy();

      public ContextClass() { this(null); }

      public ContextClass(Stragegy stragegy) { if(strategy != null) { this.strategy = strategy; } else { this.strategy = DEFAULT_STRATEGY; } }

      public int sumAll(List<Integer> numbers) { int total = 0; for (int number : numbers) { if (strategy.test(number)) { total += number; } }

        return total;
      

      } }

      // 調用 ContextClass context = new ContextClass(); context.sumAll(numbers);</pre>

           設計模式在這里發揮了作用,OO特性還是蠻強大的!但是這是唯一的解決方案嗎(當然不考慮用其他設計模式來解決,因為都是OO范疇!)?當然有,該輪到Java 8 Lambda表達式中的謂詞(Predicate)該發揮作用了!

      public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
          int total = 0;
          for (int number : numbers) {
              if (p.test(number)) {
                  total += number;
              }
          }
          return total;
      }

      sumAll(numbers, n -> true); sumAll(numbers, n -> n % 2 == 0); sumAll(numbers, n -> n > 3);</pre>      代碼是不是比上面簡潔很多了?語義應該也很明確,就不多解釋了,如果實在看不懂,請參考我的另外一篇文章: http://www.cnblogs.com/feichexia/archive/2012/11/15/Java8_LambdaExpression.html 從這里也可以看出未引入Lambda表達式之前的Java代碼的冗長(Java這點被很多人詬病)。

           當然C#早已經支持這種用法,用C#改寫上面的代碼如下,是不是與上面的代碼極其類似呢?

      public int SumAll(List<int> numbers, Func<int, bool> func) {
          var total = 0;
          foreach (var number in numbers) {
              if (func(number)) {
                  total += number;
              }
          }

      return total;
      

      }

      SumAll(numbers, n => true); SumAll(numbers, n => n % 2 == 0); SumAll(numbers, n => n > 3);</pre>


      3、Consumer與Loan Pattern

           比如我們有一個資源類Resource:

      public class Resource {

      public Resource() {
          System.out.println("Opening resource");
      }
      
      public void operate() {
          System.out.println("Operating on resource");
      }
      
      public void dispose() {
          System.out.println("Disposing resource");
      }
      

      }</pre>      我們必須這樣調用:

      Resource resource = new Resource();
      try {
          resource.operate();
      } finally {
          resource.dispose();
      }

           因為對資源對象resource執行operate方法時可能拋出RuntimeException,所以需要在finally語句塊中釋放資源,防止可能的內存泄漏。

           但是有一個問題,如果很多地方都要用到這個資源,那么就存在很多段類似這樣的代碼,這很明顯違反了DRY(Don't Repeat It Yourself)原則。而且如果某位程序員由于某些原因忘了用try/finally處理資源,那么很可能導致內存泄漏。那咋辦呢?Java 8提供了一個Consumer接口,代碼改寫為如下:

      public class Resource {

      private Resource() {
          System.out.println("Opening resource");
      }
      
      public void operate() {
          System.out.println("Operating on resource");
      }
      
      public void dispose() {
          System.out.println("Disposing resource");
      }
      
      public static void withResource(Consumer<Resource> consumer) {
          Resource resource = new Resource();
          try {
              consumer.accept(resource);
          } finally {
              resource.dispose();
          }
      }
      

      }</pre>      調用代碼如下:

      Resource.withResource(resource -> resource.operate());
            外部要訪問Resource不能通過它的構造函數了(private),只能通過withResource方法了,這樣代碼清爽多了,而且也完全杜絕了因人為疏忽而導致的潛在內存泄漏。


      4、stream+laziness => efficiency

           像之前一樣先來一段非常簡單的代碼:

      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

      for (int number : numbers) { if (number % 2 == 0) { int n2 = number * 2; if (n2 > 5) { System.out.println(n2); break; } } }</pre>      這段代碼有什么問題? 沒錯,可讀性非常差。第一步,我們利用《重構》一書中的最基礎的提取小函數重構手法來重構代碼如下:

      public boolean isEven(int number) {
          return number % 2 == 0;
      }

      public int doubleIt(int number) { return number * 2; }

      public boolean isGreaterThan5(int number) { return number > 5; }

      for (int number : numbers) { if (isEven(number)) { int n2 = doubleIt(number); if (isGreaterThan5(n2)) { System.out.println(n2); break; } } }</pre>      OK,代碼的意圖清晰多了,但是可讀性仍然欠佳,因為循環內嵌套一個if分支,if分支內又嵌套另外一個分支,于是繼續重構代碼如下:

      public boolean isEven(int number) {
          return number % 2 == 0;
      }

      public int doubleIt(int number) { return number * 2; }

      public boolean isGreaterThan5(int number) { return number > 5; }

      List<Integer> l1 = new ArrayList<Integer>(); for (int n : numbers) { if (isEven(n)) l1.add(n); }

      List<Integer> l2 = new ArrayList<Integer>(); for (int n : l1) { l2.add(doubleIt(n)); }

      List<Integer> l3 = new ArrayList<Integer>(); for (int n : l2) { if (isGreaterThan5(n)) l3.add(n); }

      System.out.println(l3.get(0));</pre>      現在代碼夠清晰了,這是典型的“流水線”風格代碼。但是等等,現在的代碼執行會占用更多空間(三個List)和時間,我們來分析下。首先第二版代碼的執行流程是這樣的:

      isEven: 1
      isEven: 2
      doubleIt: 2
      isGreaterThan5: 2
      isEven: 3
      isEven: 4
      doubleIt: 4
      isGreaterThan5: 4
      8

           而我們的第三版代碼的執行流程是這樣的:

      isEven: 1
      isEven: 2
      isEven: 3
      isEven: 4
      isEven: 5
      isEven: 6
      doubleIt: 2
      doubleIt: 4
      doubleIt: 6
      isGreaterThan5: 2
      isGreaterThan5: 4
      isGreaterThan5: 6
      8

           步驟數是13:9,所以有時候重構得到可讀性強的代碼可能會犧牲一些運行效率(但是一切都得實際衡量之后才能確定)。那么有沒有“三全其美”的實現方法呢?即:

      1. 代碼可讀性強
      2. 代碼執行效率不比第一版代碼差
      3. 空間消耗小
      4. </ol>

             Streams come to rescue! Java 8提供了stream方法,我們可以通過對任何集合對象調用stream()方法獲得Stream對象,Stream對象有別于Collections的幾點如下:

        1. 不存儲值:Streams不會存儲值,它們從某個數據結構的流水線型操作中獲取值(“酒肉穿腸過”
        2. 天生的函數編程特性:對Stream對象操作能得到一個結果,但是不會修改原始數據結構
        3. Laziness-seeking(延遲搜索):Stream的很多操作如filter、map、sort和duplicate removal(去重)可以延遲實現,意思是我們只要檢查到滿足要求的元素就可以返回
        4. 可選邊界:Streams允許Client取足夠多的元素直到滿足某個條件為止。而Collections不能這么做
        5. </ol>

               上代碼:

          System.out.println(
              numbers.stream()
                      .filter(Lazy::isEven)
                      .map(Lazy::doubleIt)
                      .filter(Lazy::isGreaterThan5)
                      .findFirst()
          );
                現在的執行流程是:
          isEven: 1
          isEven: 2
          doubleIt: 2
          isGreaterThan5: 4
          isEven: 3
          isEven: 4
          doubleIt: 4
          isGreaterThan5: 8
          IntOptional[8]
                流程基本和第二版代碼一致,這歸功于Laziness-seeking特性。怎么理解呢?讓我來構造下面這個場景:
          Stream流對象要經過下面這種流水線式處理:
          過濾出偶數 => 乘以2 => 過濾出大于5的數 => 取出第一個數

          注意:=> 左邊的輸出是右邊的輸入</pre>      而Laziness-seeking意味著 我們在每一步只要一找到滿足條件的數字,馬上傳遞給下一步去處理并且暫停當前步驟。比如先判斷1是否偶數,顯然不是;繼續判斷2是否偶數,是偶數;好,暫停過濾偶數操作,將2傳遞給下一步乘以2,得到4;4繼續傳遞給第三步,4不滿足大于5,所以折回第一步;判斷3是否偶數,不是;判斷4是否偶數,是偶數;4傳遞給第二步,乘以2得到8;8傳遞給第三步,8大于5;所以傳遞給最后一步,直接取出得到IntOptional[8]。

               IntOptional[8]只是簡單包裝了下返回的結果,這樣有什么好處呢?如果你接觸過Null Object Pattern的話就知道了,這樣可以避免無謂的null檢測。

               本文完,希望對大家有所幫助,O(∩_∩)O


          參考自:

          http://java.dzone.com/articles/why-we-need-lambda-expressions

          http://java.dzone.com/articles/why-we-need-lambda-expressions-0

          來自:http://my.oschina.net/feichexia/blog/119805

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!