由淺入深體驗 Stream 流

Stream 流是 Java 8 新提供給開發者的一組操作集合的 API,將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選、排序、聚合等。元素流在管道中經過中間操作(intermediate operation)的處理,最後由終端操作 (terminal operation) 得到前面處理的結果。Stream 流可以極大的提高開發效率,也可以使用它寫出更加簡潔明瞭的代碼。我自從接觸過 Stream 流之後,可以說對它愛不釋手。本文將由淺及深帶您體驗 Stream 流的使用。那麼就讓我們從流的簡單使用來開啟體驗之旅。

流的簡單使用

本節將通過實際的例子帶您一起了解 Stream 流:創建流以及簡單的使用,並且將其與 Java 8 之前的實現方式做一下對比。

我們將創建一個學生類 student,它包含姓名 name 和分數 score 兩個屬性。並且初始化一個學生的集合,然後分別通過 Stream 流和 Java 7 的集合操作實現篩選未及格(分數 < 60 分)的學生名單。

創建流

有以下兩種創建流的方式,第一種方式我們使用的會相對較多。

調用集合的 stream() 方法或者 parallelStream() 方法創建流。Stream 類的靜態 of() 方法創建流。

清單 1. 創建 Stream 流

使用流

清單 2 展示瞭如何使用 Stream 流篩選未及格學生名單:

使用 Stream 流篩選未及格學生名單

而使用 Java 7 實現篩選未及格學生名單所需代碼相對冗長,如清單 3 所示:

Java 7 實現篩選未及格學生名單

對比兩段代碼,我們很容易看出來 Stream 流可以讓我操作集合的代碼更加簡潔,而且可以很清晰地體現出來我們是在做一個篩選的動作,在某些情況下可以讓我們的代碼更加易讀。

流的基礎知識

接下來您將瞭解 Stream 流的基礎知識,這部分的內容將有助於您理解流的相關操作。

流的分類

Stream 流分為順序流和並行流,所謂順序流就是按照順序對集合中的元素進行處理,而並行流則是使用多線程同時對集合中多個元素進行處理,所以在使用並行流的時候就要注意線程安全的問題了。後文會單獨講解並行流的使用。

終端操作和中間操作

終端操作會消費 Stream 流,並且會產生一個結果,比如 iterator() 和 spliterator()。如果一個 Stream 流被消費過了,那它就不能被重用的。

中間操作會產生另一個流。需要注意的是中間操作不是立即發生的。而是當在中間操作創建的新流上執行完終端操作後,中間操作指定的操作才會發生。流的中間操作還分無狀態操作和有狀態操作兩種。

在無狀態操作中,在處理流中的元素時,會對當前的元素進行單獨處理。比如,過濾操作,因為每個元素都是被單獨進行處理的,所有它和流中的其它元素無關。在有狀態操作中,某個元素的處理可能依賴於其他元素。比如查找最小值,最大值,和排序,因為他們都依賴於其他的元素。

流接口

下面是一張 Stream 的 UML (統一建模語言) 類圖,後文會講解其中的一些關鍵方法。

圖 1. Stream UML 類圖

BaseStream 接口

從上面的 UML 圖可以看出來 BaseStream 接口是 Stream 流最基礎的接口,它提供了所有流都可以使用的基本功能。BaseStream 是一個泛型接口,它有兩個類型參數 T 和 S, 其中 T 指定了流中的元素的類型,S 指定了具體流的類型,由 > 可以知道 S 必須為 BaseStream 或 BaseStream 子類,換句話說,就是 S 必須是擴展自 BaseStream 的。BaseStream 繼承了 AutoCloseable 接口,簡化了關閉資源的操作,但是像平時我們操作的集合或數組,基本上都不會出現關閉流的情況。下面是 BaseStream 接口下定義的方法的相關解釋:

Iterator iterator():獲取流的迭代器。Spliterator spliterator():獲取流的 spliterator。boolean isParallel():判斷一個流是否是並行流,如果是則返回 true,否則返回 false。S sequential():基於調用流返回一個順序流,如果調用流本身就是順序流,則返回其本身。S parallel():基於調用流,返回一個並行流,如果調用流本身就是並行流,則返回其本身。S unordered():基於調用流,返回一個無序流。S onClose(Runnable closeHandler):返回一個新流,closeHandler 指定了該流的關閉處理程序,當關閉該流時,將調用這個處理程序。void close():從 AutoCloseable 繼承來的,調用註冊關閉處理程序,關閉調用流(很少會被使用到)。清單 4 列舉了由 BaseStream 接口派生出來的流接口,包括了 IntStream,LongStream,Stream 以及 DoubleStream。其中 Stream 接口最為通用,本文的主要講解對象也是它。由 BaseStream 接口派生出的流接口

Stream 接口

Stream filter(Predicate predicate):產生一個新流,其中包含調用流中滿足 predicate 指定的謂詞元素,即篩選符合條件的元素後重新生成一個新的流。(中間操作)Stream map(Function mapper),產生一個新流,對調用流中的元素應用 mapper,新 Stream 流中包含這些元素。(中間操作)IntStream mapToInt(ToIntFunction mapper):對調用流中元素應用 mapper,產生包含這些元素的一個新 IntStream 流。(中間操作)Stream sorted(Comparator comparator):產生一個自然順序排序或者指定排序條件的新流。(中間操作)void forEach(Consumer action):遍歷了流中的元素。(終端操作)Optional min(Comparator comparator) 和 Optional max(Comparator comparator):獲得流中最大最小值,比較器可以由自己定義。(終端操作)boolean anyMatch(Predicate super T> predicate):判斷 Stream 流中是否有任何符合要求的元素,如果有則返回 ture,沒有返回 false。(終端操作)Stream distinct(),去重操作,將 Stream 流中的元素去重後,返回一個新的流。(中間操作)

流的 API 操作

縮減操作

什麼是縮減操作呢?最終將流縮減為一個值的終端操作,我們稱之為縮減操作。在上一節中提到的 min(),max()方法返回的是流中的最小或者最大值,這兩個方法屬於特例縮減操作。而通用的縮減操作就是指的我們的 reduce()方法了,在 Stream 類中 reduce 方法有三種簽名方法,如下所示:

清單 5. reduce() 方法的三種實現

由上面的代碼可以看出,在 Stream API 中 reduce()方法一共存在著三種簽名,而這三種簽名則分別會適用在不同的場景,我們下面就一起來看一下如何使用。

第一種簽名

在下面的代碼中我們將對一個 Integer 類型的集合做求和操作。

清單 6. 第一種簽名的 reduce() 的使用

第二種簽名

與第一種簽名不同的是多接收了一個參數 identity,在首次執行 accumulator 表達式的時候它的第一個參數並不是 Stream 流的第一個元素,而是 identity。比如下面的例子最終輸出的結果是 Stream 流中所有元素乘積的 2 倍。

清單 7. 第二種簽名的 reduce() 的使用

第三種簽名

前面兩種前面的一個缺點在於返回的數據都只能和 Stream 流中元素類型一致,但這在某些情況下是無法滿足我們的需求的,比如 Stream 流中元素都是 Integer 類型,但是求和之後數值超過了 Integer 能夠表示的範圍,需要使用 Long 類型接受,這就用到了我們第三種簽名的 reduce() 方法。

清單 8. 第三種簽名的 reduce() 的使用

總的來說縮減操作有兩個特點,一是他只返回一個值,二是它是一個終端操作。

映射

可能在我們的日常開發過程中經常會遇到將一個集合轉換成另外一個對象的集合,那麼這種操作放到 Stream 流中就是映射操作。映射操作主要就是將一個 Stream 流轉換成另外一個對象的 Stream 流或者將一個 Stream 流中符合條件的元素放到一個新的 Stream 流裡面。

在 Stream API 庫中也提供了豐富的 API 來支持我們的映射操作,清單 9 中的方法都是我們所講的映射操作。

清單 9. 映射操作相關方法定義

其中最通用的應該就屬 mapv 和 flatMap 兩個方法了,下面將以不同的例子分別來講解著兩個方法。

map() map() 方法可以將一個流轉換成另外一種對象的流,其中的 T 是原始流中元素的類型,而 R 則是轉換之後的流中元素的類型。通過下面的代碼我們將一個學生對象的 Stream 流轉換成一個 Double 類型(學生的分數)的 Stream 流並求和後輸出。

清單 10. map() 方法的使用示例

當然上面這種情況用 mapToDouble() 會更加方便,使用 map() 是為了展示一下 map 的使用方式,那麼使用 mapToDouble() 方法的代碼如下:

清單 11. mapToDouble() 方法的使用示例

flatMap()

flatMap()操作能把原始流中的元素進行一對多的轉換,並且將新生成的元素全都合併到它返回的流裡面。假如現每個班的學生都學了不同的課程,現在需要統計班裡所有學生所學的課程列表,該如何實現呢?

清單 12. flatMap () 方法的使用示例

如上代碼中 flatMap() 中返回的是一個一個的 String 類型的 Stream 流,它們會被合併到最終返回的 Stream 流(String 類型)中。而後面的 distinct() 則是一個去重的操作,collect() 是收集操作。

收集操作

很多時候我們需要從流中收集起一些元素,並以集合的方式返回,我們把這種反向操作稱為收集操作。對於收集操作,Stream API 也提供了相應的方法。

清單 13. 收集操作相關 API

其中 R 指定結果的類型,T 指定了調用流的元素類型。內部積累的類型由 A 指定。collector 是一個收集器,指定收集過程如何執行,collect() 方法是一個終端方法。一般情況我們只需要藉助 Collectors 中的方法就可以完成收集操作。

Collectors 類是一個最終類,裡面提供了大量的靜態的收集器方法,藉助他,我們基本可以實現各種複雜的功能了。

清單 14. Collectors

Collectors 給我們提供了非常豐富的收集器,這裡只列出來了 toList 和 toMap 兩種,其他的可以參考 Collectors 類的源碼。toList() 相信您在清單 14 中已經見到了,那麼下面將展示如何將一個使用收集操作將一個 List 集合轉為 Map。

清單 15. 使收集操作將 List 轉 Map

可以看到通過 Stream API 可以很方便地將一個 List 轉成了 Map,但是這裡有一個地方需要注意。那就是在通過 Stream API 將 List 轉成 Map 的時候我們需要確保 key 不會重複,否則轉換的過程將會直接拋出異常。

並行流的使用

我們處於一個多核處理器的時代,在日常的開發過程中也經常會接觸到多線程。Stream API 也提供了相應的並行流來支持我們並行地操作數組和集合框架,從而高速地執行我們對數組或者集合的一些操作。

其實創建一個並行流非常簡單,在創建流部分已經提到過如何創建一個並行流,我們只需要調用集合的 parallelStream() 方法就可以輕鬆的得到一個並行流。相信大家也知道多線程編程非常容易出錯,所以使用並行流也有一些限制,一般來說,應用到並行流的任何操作都必須符合三個約束條件:無狀態、不干預、關聯性。因為這三大約束確保在並行流上執行操作的結果和在順序流上執行的結果是相同的。

縮減操作部分我們一共提到了三種簽名的 reduce() 方法,其中第三種簽名的 reduce() 方法最適合與並行流結合使用。

清單 16. 第三種簽名方式的 reduce() 方法與並行流結合使用

其中 accumulator 被為累加器,combiner 為合成器。combiner 定義的函數將 accumulator 提到的兩個值合併起來,在之前的例子中我們沒有為合併器設置具體的表達式,因為在那個場景下我們不會使用到合併器。下面我們來看一個例子,並且分析其執行的步驟:

清單 17. 並行流使用場景

上面的代碼實際上是先使用累加器把 Stream 流中的兩個元素都加 2 後,然後再使用合併器將兩部分的結果相加。最終得到的結果也就是 8。並行流的使用場景也不光是在這中縮減操作上,比如我會經常使用並行流處理一些複雜的對象集合轉換,或者是一些必須循環調用的網絡請求等等,當然在使用的過程中最需要注意的還是線程安全問題。

在本教程中,我們主要了解了 Java 8 Stream 流的基礎知識及使用,涵蓋 Stream 流的分類、接口、相關 API 操作以及並行流的使用。