Java SE 8 Lambda 標準庫概覽

jopen 10年前發布 | 21K 次閱讀 Java Java開發

Java SE 8 中加入了新的語言特性,主要包括Lambda表達式,和默認方法JSR335 對這些新特性進行了詳細描述,并且OpenJDK Lambda 項目實現了這些新特性為了更好的利用這些新特性Java SE 8 的核心標準庫也做了相應的修改和增強。這篇文章主要描述核心庫中的新特性 

1,背景

    假如Java語言最初就包括了Lambda表達式,那么Collection API會與現在的大不相同的。隨著JSR335 中加入Lambda表達式,一個不幸的影響是我們的Collection API過時了。你可能會去重新開始構建一個新的集合框架(“Collection II”),但是當集合接口已經滲入整個Java生態系統并且被使用多年時,替換Collection框架將會是艱巨而復雜的任務。取而代之,我們追求演化的策略,如在當前接口(如Collection,List,Iterable)中添加拓展方法,添加Stream(如java.util.stream.Stream)的概念來執行數據集上的聚合操作,改裝現有的類提供Stream視圖,以此實現在不替換用戶習慣使用的ArrayList和HashMap的同時,應用新的語法。(這不是說Collection 框架就不會被替換了,顯然,他的局限性已不單單是限制了Lambda的設計,JDK未來的版本中可能會考慮更現代的集合框架)

    這一工作的關鍵驅動因素是使得開發者能更好的接受并行開發。既然Java平臺已經提供了強大的并發與并行的支持,那么當開發者嘗試從串行遷移到并行時面臨的障礙就不必要了。因此提供串行并行友好的語法變的異常重要。這使得將關注點從怎樣執行轉到執行什么變得容易。在并行的易用性(目前還并非如此)與透明化之間的權衡非常重要,我們的目標是明確但不明顯的并行。(使得并行透明化會導致數據競爭時的不確定性和可能性)

 

2,內部迭代與外部迭代

    集合框架依賴于Collection接口提供的外部迭代的概念,這一概念通過Iterable接口實現,是一種枚舉集合元素的手段,客戶端使用它順序訪問集合中的元素。舉個例子,如果你想將一個Shape集合中每一個shape的顏色設為紅色,可以像這樣寫

for (Shape s : shapes) {
    s.setColor(RED);
}

    這個例子解釋了外部迭代;for-each循環調用 Shape集合的 iterator()方法,并且一個一個的訪問集合元素。外部迭代已經足夠簡潔直接了,但是它依舊有幾個問題:

  • Java的for循環本質上是串行的,并且必須按序處理集合中的元素。

    </li>

  • 它剝奪了庫方法管理控制流程的機會,它可以利用數據的重排序,并行,短路效應,懶惰性獲取更好的性能。

    </li> </ul>


        有時for-each循環有力的保障(順序的,有序的)是可取的,但大多數情況下只是阻礙性能。

        外部迭代的變通方案是內部迭代,它不再控制迭代,客戶端將它委托給庫,并傳入代碼片段,然后會以并行的方式進行計算。

        內部迭代等同于之前的例子:

    shapes.forEach(s -> s.setColor(RED));

        這看上去像是一個小的語法改變,但差異卻是重大的。操作的控制已經從客戶端代碼轉移到了庫代碼,這不僅允許庫抽象常見的控制流操作,并且使得他們具有使用懶惰性,并行,無序的執行方式來提供性能。(forEach的實現是否是真的做了這些事情,是由它的實現決定的,但是內部迭代有這種可能性,然而外部迭代卻沒有)

        然而外部迭代混淆了what(設置shapes的顏色為紅色)和how(獲取Iterator并且順序迭代),內部迭代允許客戶端指定what,然后由庫來控制how。這帶來了幾個潛在的好處:客戶端代碼更加清晰,因為他只需要關注問題的陳述,而不是怎樣去解決它的細節,并且我們可以將復雜的優化代碼移到庫中,這會讓所有人受益。

     

    3,Streams

        Java SE 8 新標準庫中引入的新概念是stream,在包java.util.stream 中定義的。(有多種stream類型;Stream<T> 表示一個對象引用的stream,還有一些特殊化的stream, 如表示基本類型的IntStream ) stream表示一系列值得序列,并且暴露了一組聚合操作的接口,使得我們可以簡單清晰的在這些值上進行常用的計算。庫提供了方便的途徑來獲取集合,數組,其他數據源上的stream視圖。

        Stream操作鏈接在一起形成管道。舉個例子,如果我們希望只有藍色的shape顏色設為紅色,那么我們可以說:

    shapes.stream() 
          .filter(s -> s.getColor() == BLUE)
          .forEach(s -> s.setColor(RED));


        Collection 接口上的stream()方法會產生一個集合元素上的stream視圖;filter操作產生一個只包含藍色shape的stream,并且forEach操作將這些元素設為紅色。

        如果我們想要收集藍色shape到一個新的列表,我們可以說:

    List<Shape> blue = shapes.stream()
                             .filter(s -> s.getColor() == BLUE)
                             .collect(Collectors.toList());


        collect()操作收集輸入的元素到一個集合體(像是List),或者是一個概述;collect()的參數指定聚合操作的行為。在這里我們使用了toList(),這是一個簡單的將元素收集到List的方式。(更多colle()的細節可以在“Collectors”一節找到)

        如果每一個shape都包含在一個Box里,我們想知道哪一些box包含了至少一個藍色的shape,我們可以說:

    Set<Box> hasBlueShape = shapes.stream()
                                  .filter(s -> s.getColor() == BLUE)
                                  .map(s -> s.getContainingBox())
                                  .collect(Collectors.toSet());

     

        map()操作產生一個對輸入stream中每個元素應用mapping函數后的結果的stream(這里,mapping函數取得一個shape并且返回它的包含Box)。

        如果我們想要計算藍色shape的總重量,我們可以這樣:

    int sum = shapes.stream()
                    .filter(s -> s.getColor() == BLUE)
                    .mapToInt(s -> s.getWeight())
                    .sum();

        目前為止,我們還沒有提供更多關于stream操作展示的具體簽名;這些列子簡單的解釋了Streams框架要解決的問題類型。

     

    4, Streams 與 Collections

        集合框架與流框架雖然看上去很相似,但是卻有著不同的目標。集合框架主要關注的是高效的管理和訪問元素。相反,流框架不提供直接訪問或操作元素的手段,而是陳述式的描述在數據源上執行的總計操作。因此,流式框架與集合框架有以下幾點不同:

    • 沒有存儲。流不會存儲數據值;他們會將源數據(可能是一個數據結構,一個生成函數,或者I/0通道等)通過一個計算步驟的管道。

      </li>

    • 功能性。在流上操作會生成一個結果,但是不會修改它的底層數據結構。

      </li>

    • 延遲查找。許多流操作(像是過濾,排序,映射,或者去重)都可以實現為延遲的。這有助于整個管道上進行高效的單次執行。同樣有助于高效的短路操作。

      </li>

    • 可選的范圍。有很多問題去合理的表示一個無限的流,并讓客戶端滿意的消耗這些值。(如果我們窮舉完全數,很容易通過在一個integer流上執行過濾操作來表達)集合被約束為有限的,而流則不是。(一個無限數據源的流管道可以使用短路操作來在有限的時間內終止操作; 或者,你可以在流上請求一個Iterator來手動迭代)。

      </li> </ul>

          作為API,Streams框架是完全獨立于Collections框架的。然而將集合作為流的數據源(Collection接口提供了stream()和parallelStream()方法)或者將流轉儲為集合(使用前面展示的collect()操作)是非常容易的。集合外的聚合結果也可以作為流的數據源。許多JDK類,像是BufferedReader,Random 和 BitSet都被改寫為可以作為流的數據源,并且Arrays.stream()方法提供了數組的流視圖。事實上,任何可以被描述為Iterator的都可以用作流的數據源,如果有更多可用信息(像是流內容的分類源數據或者大小),庫可以提供一個優化的操作。

       

      5,延遲

          像過濾或者映射這樣的操作,既可以被立即執行(即過濾操作會在方法返回前便對所有元素執行了過濾)也可以是延遲的(這里流只表示在數據源上應用了過濾操作之后的過濾結果)。實際上延遲的執行計算是很有益的。舉個例子,如果我們執行延遲過濾,我們可以在管道內將過濾操作與之后的其他操作融合執行,以免在一個數據源上執行多個操作。相似的,如果我們想要在一個大的集合中查詢滿足條件的第一個元素,那么我們可以找到一個后便停止查找,而不用處理整個的集合。(對于無限的數據源這尤其重要;延遲對有限數據源僅僅是優化,然而它卻使得對無限數據源的操作稱謂可能,而及早的方式將永遠不會停止。)

          過濾和映射操作可以被認為是天生的延遲,無論他們是否被這樣實現。另一方面,產生值得操作如sum(),或者產生副作用的操作如forEach()是“天生饑渴的”,因為他們必須產生具體的結果。

          在一個管道中像是:

      int sum = shapes.stream()
                      .filter(s -> s.getColor() == BLUE)
                      .mapToInt(s -> s.getWeight())
                      .sum();

      過濾與映射操作時延遲的。這意味著知道我們開始sum操作我們才會從數據源中抽取數據,并且當我們執行sum時,我們會將過濾,映射,以及求和等操作融合在一起執行。這最小化了那些需要對中間元素管理的統計成本。

          許多循環可以被重述為數據源(數組,集合,生成方程,IO通道)上的聚合操作, 做一系列的延遲操作(過濾,映射等),以及一次即可操作(forEach,toArray,collect等) -- 如過濾-映射-求和,過濾-映射-排序-迭代等。天生延遲操作傾向于被用來計算臨時的中間結果,我們在API中利用了這一屬性。我們并非讓filter和map返回一個集合,取而代之的是返回一個新的流。在Stream API中,返回流的操作時延遲的,返回非流結果的操作(或沒有結果的,像forEach)是饑渴的。大多數情況下,潛在的延遲操作被用于聚合,這被證明是我們確切想要的 -- 每一個階段都需要一個流作為輸入,在流上執行一些轉換,然后在管道中將值傳遞到下一個階段。

          當我們在管道中使用數據源-延遲-延遲-即可模式時,延遲是無形的,因為延遲計算是被夾在了數據源(通常是集合)和產生預期結果(或副作用)的操作之間的。這被證明在一個相對小范圍的API中會產生好的性能和可用性。

          anyMatch(Predicate)或者findFirst()方法,雖然是即可的,但是一旦他們確定最終結果,便可使用短路來停止他們。

      像下面的管道:

      Optional<Shape> firstBlue = shapes.stream()
                                        .filter(s -> s.getColor() == BLUE)
                                        .findFirst();

      因為過濾是延遲的,findFirst將從流的上游抽取數據直到獲得元素,這意味我們只需對輸入元素應用斷言(predicate)直到我們找到一個斷言正確的元素,而不是所有的。findFirst()方法返回一個Optional,因為可能沒有元素滿足想要的條件。Optional提供了一種描述可能存在或可能不存在的值得方法。

          記住,用戶沒必要要求延遲操作,或者根本不用考慮這些;庫會安排這一切,并保證正確和盡可能小的計算。

       

      6,并行

          流管道既可以被串行執行,也可以被并行執行;這一選擇是流的一個屬性。除非你顯示的請求一個并行流,否則JDK實現總是返回一個串行流(parallel()方法可以將串行流轉換為并行流。)

          雖然并行總是顯示的,但它不是侵入式的。通過在數據源上簡單的調用parallelStream()方法,便可以使計算重量總和的例子以并行方式執行:

      int sum = shapes.parallelStream()
                      .filter(s -> s.getColor() == BLUE)
                      .mapToInt(s -> s.getWeight())
                      .sum();

       

          結果是同樣計算的串行和并行表達式看起來非常的相似,但是并行執行依舊被清晰的標記為并行(而不是并行機械結構壓倒代碼)。

          因為流數據源可能是不可變的集合,那么如果遍歷數據源時被修改就有可能產生沖突。流操作通常是在底層數據源在被操作期間保持不變的情況下使用。這一條件通常是易于維護的;如果集合被限制在當前線程中,那么簡單的確保傳遞給流操作的Lambda表達式不會改變流的數據源即可。(這一條件與目前在迭代集合時的限制有著本質上的不同;如果一個集合在被迭代時被修改了,大部分的實現會拋出ConcurrentModificationException 異常。)我們把這一需求當作‘無干擾’。

          最好是避免傳遞給流方法的Lambda表達式的副作用。雖然一些副作用是安全的,像是打印輸出值一類的調試語句,但是從這些Lambda中訪問可變狀態值可能會引起數據競爭或者其他奇怪的行為,因為Lambda可能會被多個線程同時執行,并且可能無法看到元素的自然順序。無干擾不僅包括不干擾數據源,而且不干擾其他Lambda;當一個Lambda修改一個可變狀態值,而另一個Lambda試圖讀取它時可能會產生這種干擾。

          只要滿足了無干擾的需求,我們就可以安全的執行并行操作并獲得預期的結果了,即使數據源不是線程安全的,像是ArrayList。

       

      7,例子

          下面是JDK Class類中的一個代碼片段(getEnclosingMethod()方法),它會循環便利所有聲明的方法,匹配方法名稱,返回類型,以及參數數量和類型。下面是原始代碼:

      for (Method m : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
           if (m.getName().equals(enclosingInfo.getName()) ) {
               Class<?>[] candidateParamClasses = m.getParameterTypes();
               if (candidateParamClasses.length == parameterClasses.length) {
                   boolean matches = true;
                   for(int i = 0; i < candidateParamClasses.length; i++) {
                       if (!candidateParamClasses[i].equals(parameterClasses[i])) {
                           matches = false;
                           break;
                       }
                   }

                   if (matches) { // finally, check return type                  if (m.getReturnType().equals(returnType) )                      return m;              }          }      }  }

       throw new InternalError("Enclosing method not found");</pre>

          使用流,我們可以去除所有這些臨時變量并且控制邏輯移到庫中去。我們通過反射獲得方法的列表,然后使用Arrays.stream將它轉換為Stream,并且使用一些列的過濾器來排除那些不匹配名稱,參數類型和返回值的方法。findFirst()的結果是一個Optional<Method>,然后我們可以獲取并返回結果方法,或者拋出異常。

          

      return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods())
                   .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
                   .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
                   .filter(m -> Objects.equals(m.getReturnType(), returnType))
                   .findFirst()
                   .orElseThrow(() -> new InternalError("Enclosing method not found");

          這個版本的代碼更加的緊湊,可讀,不易出錯。

          流操作對于集合上的特定查詢時非常有效的。考慮一個假象的‘音樂庫’應用,這個庫中有一系列專輯,每個專輯有一個標題和一些列歌曲,每個歌曲有一個名字,藝術家,和評價等級。

          考慮這樣一個查詢“找出至少有一個評價等級大于等于4的歌曲的專輯名稱,并按名稱排序”,我們可以這樣去構造這一集合:

      List<Album> favs = new ArrayList<>();
      for (Album a : albums) {
          boolean hasFavorite = false;
          for (Track t : a.tracks) {
              if (t.rating >= 4) {
                  hasFavorite = true;
                  break;
              }
          }
          if (hasFavorite)
              favs.add(a);
      }
      Collections.sort(favs, new Comparator<Album>() {
                                 public int compare(Album a1, Album a2) {
                                     return a1.name.compareTo(a2.name);
                                 }});

          我們可以使用流操作來簡化這三個主要步驟---判斷一個專輯中是否有歌曲等級至少是多少(anyMatch()),排序,以及將符合條件的專輯放入List中:

      List<Album> sortedFavs =
        albums.stream()
              .filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
              .sorted(Comparator.comparing(a -> a.name))
              .collect(Collectors.toList());

          Comparator.comparing()方法接受一個函數,該函數提取一個Comparable的排序鍵,并且返回一個以此鍵進行比較的Comparator(請看下面“Comparator 工廠”部分)

       

      來自:http://my.oschina.net/HeliosFly/blog/192457

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