Stream深入分析

關於jdk8的重大改進,其中java對函數式編程的重視程度看看加入函數式編程後,擴充了多少功能,重寫了多少基礎類庫可見一斑。引入Lambda表達式和Stream也是工作中應用最廣泛的。關於Stream推薦參考《Java8函數式編程》。

一、Stream初始化

       stream初始化的方式主要有以下幾種:

  • 調用Collection.stream()或者Collection.parallelStream()方法
  • 調用Arrays.stream(T[] array)方法

雖然Stream是通過  Collection.stream()初始化的但是和Collections有以下不同之處:

  • 無存儲stream不是一種數據結構,它只是某種數據源的一個視圖,數據源可以是一個數組,Java容器或I/O channel等。
  • 爲函數式編程而生。對stream的任何修改都不會修改背後的數據源,比如對stream執行過濾操作並不會刪除被過濾的元素,而是會產生一個不包含被過濾元素的新stream
  • 惰式執行stream上的操作並不會立即執行,只有等到用戶真正需要結果的時候纔會執行。
  • 可消費性stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。

    下面具體瞭解一下Stream流初始化的源碼:

圖一:Collection.stream()

 最終都是調用到 StreamSupport.stream這個方法。我們看到這裏第一個參數是獲取一個Spliterator的實例,它表示從數據源中獲取元素的方式。相當於升級版的Iterator。第二個參數是是否並行。大概介紹一下Spliterator接口方法。

  • boolean tryAdvance(Consumer action); 該方法會處理每個元素,如果沒有元素處理,則應該返回false,否則返回true。
  • default void forEachRemaining(Consumer action) 對每個剩餘元素執行給定對動作,依次處理知道所有元素被處理或者異常終止,默認方法調用tryAdvance方法。
  • Spliterator trySplit(); 將一個Spliterator分割成多個Spliterator。分割的Spliterator被用於每個子線程進行處理,從而達到併發處理的效果返回一個新對Spliterator迭代器。
  • long estimateSize();用於估算還剩下多少個元素需要遍歷。
  • int characteristics(); 給出stream流具有的特性,不同的特性,不僅是會對流的計算有優化作用,更可能對計算結果會產生影響。如:Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED。
    • default Comparator getComparator() 對sorted的流,給出比較器。如果Spliterator對list是通過Comparator排序對,則返回Comparator。如果list是自然排序返回null,其他情況下拋錯。

這裏主要講Stream關於Spliterator內部實現大家可以通過源碼瞭解一下,這裏不贅述。

>>>繼續進入StreamSupport類:

圖二:StreamSupport.stream()

我們可以看到最終調用對是ReferencePipeLine.Head,從圖四可以看到ReferencePipeLine和InitPipeLine  LongPipeLine  DoublePipeLine 都繼承AbstractPipeline和ReferencePipeLine時並行關係。之所以爲三種基本類型定製,主要用於頻繁拆裝箱。AbstractPipeline時流水線核心抽象類,用於構建和管理流水線。

>>>AbstractPipeline:

AbstractPipeline中定義類三個變量:sourceStage(源階段),previousStage(上游pipeline,前一階段),nextStage(下一階段)。

圖三:AbstractPipeline

 

 

圖四:Stream對主要接口類對關係圖

 

 

講完Stream對初始化,下面我們來了解一下Stream對中間操作,Stream對操作類型分外中間操作和結束操作

圖五:Stream操作類型

Stream操作分類              

二、中間操作(Intermediate operations)
1、返回結果是stream的非靜態方法
2、中間操作是一種lazy操作可以多個,實際上並沒有執行,而是等最終操作時執行。這也是Stream在迭代大集合時高效等原因之一。
3、中間操作分爲無狀態(Stateless)操作和有狀態(Stateful)操作,無狀態是指不依賴前面元素的影響。而有狀態必須等前面執行完成之後才能知道結果。

通常一個完整的Stream操作是由:數據源  操作  回調函數組成的雙向鏈表結構。如圖六

圖六:Stream流水線雙向鏈表結構

 

通過Collection.Head初始化,後面調用一系列調中間操作不斷產生新調Stream。這些Stream以雙向鏈表調方式組織起來,形成一個流水線。由於每個stage記錄了前一次操作調回調函數,這樣就建立起對數據源對所有操作。

前面我們也提到了這種方式可以提高大集合迭代的高效那麼stream是做的呢?

>>>Stream疊加操作

Stream通過Sink接口來協調相鄰Stage之間的調用關係。sink接口包含下面幾個方法:

  • void begin(Long size);開始遍歷元素之前調用該方法,通知Sink做好準備。
  • void end();所有元素遍歷完成之後調用。
  • boolean cancellationReauested();是否可以結束操作,可以讓短路操作儘早結束。
  • void accept(T t);遍歷元素時調用,接受一個待處理元素,並對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法裏,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。

   通過Sink接口相鄰Stage之間調用就很清晰了,每個Stage都將自己都操作封裝到一個Sink裏,前一個Stage調用後一個Stage到accept()方法。我們來看一下Sink.map的源碼。

 

Sink封裝了Stream的每一步操作,但是我們前面提到Stream的中間操作是Lazy操作,真正啓動這些操作的是結束操作來執行的。

三、結束操作(Terminal operations)
1、只能有一個最終操作,不一定有返回結果。
2、分爲非短路操作和短路操作。非短路操作所有元素處理完,短路操作不用處理全部元素就可以返回結果

以ReferencePipeline.forEachOrder爲例,PS:簡單提一下Stream.forEach不能保證順序,forEachOrder能保證順序但效率不高。直接上源碼:

 

ForEachOps是用戶創建TerminalOp實例的工廠類。TerminalOp是終止操作最頂層的一個接口。TerminalOp接口的實現類有ForEachOp, ReduceOp,FindOp, MatchOp。
先看ForEachOps.makeRef()方法:

OfRef是引用流的默認實現類,這裏新建了一個OfRef的實例,構造方法如下:

 

將我們實現的Consumer函數式接口賦值給成員變量。回到evaluate方法:

 

跟到串行流的實現,實現在ForEachOp中:

 

 

PipelineHelper類型其實是AbstractPipeline的父類,而AbstractPipeline又是ReferencePipeline的父類。再跟進helper.wrapAndCopyInto方法,是現在AbstractPipeline中:

 

可以看到通過ReferencePipeline的雙向鏈表,從最後一個操作(也就是終止操作)往前遍歷,將所有的操作都串聯起來,最終返回一個指向第一個操作的Sink引用。

對於Stream操作性能:

  • 對於複雜操作,Stream串行API性能可以和手動實現的效果匹敵,在並行執行時Stream API效果遠超手動實現。
  • 對於簡單操作,比如最簡單的遍歷,Stream串行API性能明顯差於顯示迭代,但並行的Stream API能夠發揮多核特性。

所以,如果出於性能考慮,1. 對於簡單操作推薦使用外部迭代手動實現,2. 對於複雜操作,推薦使用Stream API, 3. 在多核情況下,推薦使用並行Stream API來發揮多核優勢,4.單核情況下不建議使用並行Stream API。

如果出於代碼簡潔性考慮,使用Stream API能夠寫出更短的代碼。即使是從性能方面說,儘可能的使用Stream API也另外一個優勢,那就是隻要Java Stream類庫做了升級優化,代碼不用做任何修改就能享受到升級帶來的好處。

參考資料:

https://www.cnblogs.com/CarpenterLee/p/6637118.html

https://www.cnblogs.com/Dorae/p/7779246.html

https://www.cnblogs.com/CarpenterLee/p/6675568.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章