1. 程式人生 > >java.util.stream 庫簡介

java.util.stream 庫簡介

問題 而且 contain index ide 全面 劃分 深度優先 簡介

Java Stream簡介

  Java SE 8 中主要的新語言特性是拉姆達表達式。可以將拉姆達表達式想作一種匿名方法;像方法一樣,拉姆達表達式具有帶類型的參數、主體和返回類型。但真正的亮點不是拉姆達表達式本身,而是它們所實現的功能。拉姆達表達式使得將行為表達為數據變得很容易,從而使開發具有更強表達能力、更強大的庫成為可能。

  Java SE 8 中引入的一個這樣的庫是 java.util.stream 包 (Streams),它有助於為各種數據來源上的可能的並行批量操作建立簡明的、聲明性的表達式。較早的 Java 版本中也編寫過像 Streams 這樣的庫,但沒有緊湊的行為即數據語言特性,而且它們的使用很麻煩,以至於沒有人願意使用它們。您可以將 Streams 視為 Java 中第一個充分利用了拉姆達表達式的強大功能的庫,但它沒有什麽特別奇妙的地方(盡管它被緊密集成到核心 JDK 庫中)。Streams 不是該語言的一部分 — 它是一個精心設計的庫,充分利用了一些較新的語言特性。

關於本系列

借助 java.util.stream包,您可以簡明地、聲明性地表達集合、數組和其他數據源上可能的並行批量操作。在 Java 語言架構師 Brian Goetz 編寫的這個 系列 中,全面了解 Streams 庫,並了解如何最充分地使用它。

本文是一個深入探索 java.util.stream 庫的系列的第一部分。本期介紹該庫,並概述它的優勢和設計原理。在後續幾期中,您將學習如何使用流來聚合和匯總數據,了解該庫的內部原理和性能優化。

使用流的查詢

流的最常見用法之一是表示對集合中的數據的查詢。清單 1 給出了一個簡單的流管道示例。該管道獲取一個在買家和賣家之間模擬購買的交易集合,並計算生活在紐約的賣家的交易總價值。

清單 1. 一個簡單的流管道
1 2 3 4 5 int totalSalesFromNY = txns.stream() .filter(t -> t.getSeller().getAddr().getState().equals("NY")) .mapToInt(t -> t.getAmount()) .sum();

“流利用了這種最強大的計算原理:組合。

filter() 操作僅選擇與來自紐約的賣家進行的交易。mapToInt() 操作選擇所關註交易的交易金額。最終的 sum()

操作將對這些金額求和。

這個例子非常容易理解,即使比較挑剔的人也會發現這個查詢的命令版本(for 循環)非常簡單,而且需要更少的代碼行即可表達。為了體現流方法的好處,示例問題沒有必要變得過於復雜。流利用了這種最強大的計算原理:組合。通過使用簡單的構建塊(過濾、映射、排序、聚合)來組合復雜的操作,在問題變得比相同數據源上更加臨時的計算更復雜時,流查詢更可能保留寫入和讀取的簡單性。

作為來自清單 1 中的相同領域的更復雜查詢,考慮 “打印與年齡超過 65 歲的買家進行交易的賣家姓名,並按姓名排序。”以舊式的(命令)方式編寫此查詢可能會得到類似清單 2 的結果。

清單 2. 對一個集合的臨時查詢
1 2 3 4 5 6 7 8 9 10 11 12 13 Set<Seller> sellers = new HashSet<>(); for (Txn t : txns) { if (t.getBuyer().getAge() >= 65) sellers.add(t.getSeller()); } List<Seller> sorted = new ArrayList<>(sellers); Collections.sort(sorted, new Comparator<Seller>() { public int compare(Seller a, Seller b) { return a.getName().compareTo(b.getName()); } }); for (Seller s : sorted) System.out.println(s.getName());

盡管此查詢比第一個查詢稍微復雜一點,但很明顯采用命令方法的結果代碼的組織結構和可讀性已開始下降。讀者首先看到的不是計算的起點和終點;而是一個一次性中間結果的聲明。要閱讀此代碼,您需要在頭腦中緩存大量上下文,然後才能明白代碼的實際用途。清單 3 展示了可以如何使用 Streams 重寫此查詢。

清單 3. 使用 Streams 表達的清單 2 中的查詢
1 2 3 4 5 6 7 txns.stream() .filter(t -> t.getBuyer().getAge() >= 65) .map(Txn::getSeller) .distinct() .sorted(comparing(Seller::getName)) .map(Seller::getName) .forEach(System.out::println);

清單 3 中的代碼更容易閱讀,因為用戶既沒有被 “垃圾” 變量(比如 sellerssorted)分心,也不需要在閱讀代碼的同時跟蹤記錄大量上下文;而且代碼看起來幾乎就像問題陳述一樣。可讀性更強的代碼也更不容易出錯,因為維護者更容易一眼就看出代碼在做什麽。

Streams 登錄所采用的設計方法實現了實際的關註點分離。客戶端負責指定計算的是 “什麽”,而庫負責控制 “如何做”。這種分離傾向於與專家經驗的分發平行進行;客戶端編寫者通常能夠更好地了解問題領域,而庫編寫者通常擁有所執行的算法屬性的更多專業技能。編寫允許這種關註點分離的庫的主要推動力是,能夠像傳遞數據一樣輕松地傳遞行為,從而使調用方可在 API 中描述復雜計算的結構,然後離開,讓庫來選擇執行戰略。

流管道剖析

所有流計算都有一種共同的結構:它們具有一個流來源、0 或多個中間操作,以及一個終止操作。流的元素可以是對象引用 (Stream<String>),也可以是原始整數 (IntStream)、長整型 (LongStream) 或雙精度 (DoubleStream)。

因為 Java 程序使用的大部分數據都已存儲在集合中,所以許多流計算使用集合作為它們的來源。JDK 中的 Collection 實現都已增強,可充當高效的流來源。但是,還存在其他可能的流來源,比如數組、生成器函數或內置的工廠(比如數字範圍),而且(如本系列中的 第 3 期 所示)可以編寫自定義的流適配器,以便可以將任意數據源充當流來源。表 1 給出了 JDK 中的一些流生成方法。

表 1. JDK 中的流來源

中間操作負責將一個流轉換為另一個流,中間操作包括 filter()(選擇與條件匹配的元素)、map()(根據函數來轉換元素)、distinct()(刪除重復)、limit()(在特定大小處截斷流)和 sorted()。一些操作(比如 mapToInt())獲取一種類型的流並返回一種不同類型的流;清單 1 中的示例的開頭處有一個 Stream<Transaction>,它隨後被轉換為 IntStream。表 2 給出了一些中間流操作。

表 2. 中間流操作

中間操作始終是惰性的:調用中間操作只會設置流管道的下一個階段,不會啟動任何操作。重建操作可進一步劃分為無狀態有狀態 操作。無狀態操作(比如 filter()map())可獨立處理每個元素,而有狀態操作(比如 sorted()distinct())可合並以前看到的影響其他元素處理的元素狀態。

數據集的處理在執行終止操作時開始,比如縮減(sum()max())、應用 (forEach()) 或搜索 (findFirst()) 操作。終止操作會生成一個結果或副作用。執行終止操作時,會終止流管道,如果您想再次遍歷同一個數據集,可以設置一個新的流管道。表 3 給出了一些終止流操作。

表 3. 終止流操作

流與集合比較

  盡管流在表面上可能類似於集合(您可以認為二者都包含數據),但事實上,它們完全不同。集合是一種數據結構;它的主要關註點是在內存中組織數據,而且集合會在一段時間內持久存在。集合通常可用作流管道的來源或目標,但流的關註點是計算,而不是數據。數據來自其他任何地方(集合、數組、生成器函數或 I/O 通道),而且可通過一個計算步驟管道處理來生成結果或副作用,在此刻,流已經完成了。流沒有為它們處理的元素提供存儲空間,而且流的生命周期更像一個時間點 — 調用終止操作。不同於集合,流也可以是無限的;相應地,一些操作(limit()findFirst())是短路,而且可在無限流上運行有限的計算。

  集合和流在執行操作的方式上也不同。集合上的操作是急切和突變性的;在 List 上調用 remove() 方法時,調用返回後,您知道列表狀態會發生改變,以反映指定元素的刪除。對於流,只有終止操作是急切的;其他操作都是惰性的。流操作表示其輸入(也是流)上的功能轉換,而不是數據集上的突變性操作(過濾一個流會生成一個新流,新流的元素是輸入流的子集,但沒有從來源刪除任何元素)。

  將流管道表達為功能轉換序列可以實現多種有用的執行戰略,比如惰性短路操作融合。短路使得管道能夠成功終止,而不必檢查所有數據;類似 “找到第一筆超過 1000 美元的交易” 這樣的查詢不需要在找到匹配值後檢查其他任何交易。操作融合表示,可在數據上的一輪中執行多個操作;在 清單 1 的示例中,3 個操作組合成了數據上的一輪操作,而不是首先選擇所有匹配的交易,然後選擇所有對應的金額,最後對它們求和。

  類似 清單 1 和 清單 3 中的查詢的命令版本通常依靠物化集合來獲得中間計算的結果,比如過濾或映射的結果。這些結果不僅可能讓代碼變得雜亂,還可能讓執行變得混亂。中間集合的物化僅作用於實現,而不作用於結果,而且它使用計算周期將中間結果組織為將會被丟棄的數據結構。

  相反,流管道將它們的操作融合到數據上盡可能少的輪次中,通常為單輪。(有狀態中間操作,比如排序,可引入對多輪執行必不可少的障礙點。)流管道的每個階段惰性地生成它的元素,僅在需要時計算元素,並直接將它們提供給下一階段。您不需要使用集合來保存過濾或映射的中間結果,所以省去了填充(和垃圾收集)中間集合的工作。另外,遵循 “深度優先” 而不是 “寬度優先” 的執行戰略(跟蹤一個數據元素在整個管道中的深度),會讓被處理的操作在緩存中變得更 “熱”,所以您可以將更多時間用於計算,花更少時間來等待數據。

  除了將流用於計算之外,您可能還希望考慮通過 API 方法使用流來返回聚合結果,而在以前,您可能返回一個數組或集合。返回流的效率通常更高一些,因為您不需要將所有數據復制到一個新數組或集合中。返回流通常更加靈活;庫選擇返回的集合形式可能不是調用方所需要的,而且很容易將流轉換為任何集合類型。(返回流不合適,而返回物化集合更合適的主要情形是,調用方需要查看某個時間點的狀態的一致快照。)

並行性

  將計算構建為功能轉換的一個有益的結果是,您只需對代碼進行極少的更改,即可輕松地在順序和並行執行之間切換。流計算的順序表達和相同計算的並行表達幾乎相同。清單 4 展示了如何並行地執行 清單 1 中的查詢。

清單 4. 清單 1 的並行版本
1 2 3 4 5 int totalSalesFromNY = txns.parallelStream() .filter(t -> t.getSeller().getAddr().getState().equals("NY")) .mapToInt(t -> t.getAmount()) .sum();

“將流管道表達為一系列功能轉換,有助於實施一些有用的執行戰略,比如惰性、並行性、短路和操作融合。

  第一行將會請求一個並行流而不是順序流,這是與 清單 1 的唯一區別,因為 Streams 庫有效地從執行計算的戰略中分解出了計算的描述和結構。以前,並行執行要求完全重寫代碼,這樣做不僅代價高昂,而且往往容易出錯,因為得到的並行代碼與順序版本不太相似。

  所有流操作都可以順序或並行執行,但請記住,並行性並不是高性能的原因。並行執行可能比順序執行更快、一樣快或更慢。最好首先從順序流開始,在您知道您能夠獲得提速(並從中受益)時才應用並行性。

java.util.stream 庫簡介