解析Java8 Stream原理
一、前言
首先我們先看一個使用Stream API的示例,具體代碼如下:
這是個很簡單的一個Stream使用例子,我們過濾掉空字符串后,轉(zhuǎn)成int類型并計算出最大值,這其中包括了三個操作:filter、mapToInt、sum。相信大多數(shù)人再剛使用Stream API的時候都會有個疑問,Stream是指怎么實現(xiàn)的,是每一次函數(shù)調(diào)用就執(zhí)行一次迭代嗎?答案肯定是否,因為如果真的是每一次函數(shù)調(diào)用就執(zhí)行一次迭代,這個效率是很難接受的,Stream也不會那么受歡迎。
其實Stream內(nèi)部是通過流水線(Pipeline)的方式來實現(xiàn)的,基本思想是在迭代的時候順著流水線盡可能的執(zhí)行更多的操作,從而避免多次迭代。為了對Stream的操作有更清晰的認識,我們匯總了Stream的所有操作。
從上表可以看出Stream將所有操作分為兩類:中間操作和終止操作。其中中間操作分為無狀態(tài)和有狀態(tài),終止操作分為非短路操作和短路操作,下面是針對這幾個操作的含義說明:
1、中間操作:中間操作只是一種標(biāo)記,只有結(jié)束操作才會觸發(fā)實際計算
- 無狀態(tài):指元素的處理不受前面元素的影響;
- 有狀態(tài):有狀態(tài)的中間操作必須等到所有元素處理之后才知道最終結(jié)果,比如排序是有狀態(tài)操作,在讀取所有元素之前并不能確定排序結(jié)果。
2、終止操作:顧名思義,就是得出最后計算結(jié)果的操作
- 短路操作:指不用處理全部元素就可以返回結(jié)果;
- 非短路操作:指必須處理所有元素才能得到最終結(jié)果。
二、Stream流水線解決方案
通過上面的介紹,我們了解到Stream在執(zhí)行中間操作時僅僅是記錄,當(dāng)用戶調(diào)用終止操作時,會在一個迭代里將已經(jīng)記錄的操作順著流水線全部執(zhí)行掉。沿著這個思路,有幾個問題需要解決:
- 用戶的操作如何記錄?
- 操作如何疊加?
- 疊加之后的操作如何執(zhí)行?
2.1、操作如何記錄
圖1-1
關(guān)于操作如何記錄,在JDK源碼注釋中多次用(操作)stage來標(biāo)識用戶的每一次操作,而通常情況下Stream的操作又需要一個回調(diào)函數(shù),所以一個完整的操作是由數(shù)據(jù)來源、操作、回調(diào)函數(shù)組成的三元組來表示。而在具體實現(xiàn)中,使用實例化的ReferencePipeline來表示,即圖1-1中的Head、StatelessOp、StatefulOp的實例。接下來我們來看下Stream幾個常用方法的源碼。
code2 Collection.Stream()
code3StreamSupport.stream()
code4 ReferencePipeline.map()
從上面源碼中可以看出來,我們調(diào)用stream()方法時最終會創(chuàng)建一個Head實例來表示流操作的頭,當(dāng)調(diào)用map()方法時則會創(chuàng)建無狀態(tài)的中間操作實例StatelessOp,同樣調(diào)用其他操作對應(yīng)的方法也會生成一個ReferencePipeline實例,在這里就不一一列舉。在用戶調(diào)用一系列操作后,最終會形成一個雙向鏈表,如下圖所示:
圖1-2
2.2、操作如何疊加
上面我們說明了Stream是通過stage記錄操作,但stage只保存當(dāng)前操作,它并不知道下個stage如何操作,需要什么操作。所以要執(zhí)行的話還需要某種協(xié)議將各個stage關(guān)聯(lián)起來。jdk中就是使用Slink接口來實現(xiàn)的,Slink接口定義begin()、end()、cancellationRequested()、accept()四個方法,如下表所示。
往回看code3 ReferencePipeline.map()的方法,我們會發(fā)現(xiàn)我們在創(chuàng)建一個ReferencePipeline實例的時候,需要重寫opWrapSink方法來生成對應(yīng)Sink實例。而且通過閱讀源碼會發(fā)現(xiàn)常用的操作都會創(chuàng)建一個ChainedReference實例。我們可以看下code5 ChainedReference抽象類的源碼實現(xiàn),因為ChainedReference只是個抽象實現(xiàn),不攜帶具體操作的特性,所以是更能體現(xiàn)作者的設(shè)計理念。
通過查看源碼可以發(fā)現(xiàn)ChainedReference會持有下一個操作的Slink,并在調(diào)用begin、end、cancellationRequested方法會調(diào)用下一個操作的Slink的相應(yīng)方法,以此來達到疊加的效果。
code5ChainedReference
2.3、疊加之后的操作如何執(zhí)行
Sink完美封裝了Stream每一步操作,并給出了[處理->轉(zhuǎn)發(fā)]的模式來疊加操作。這一連串的齒輪已經(jīng)咬合,就差最后一步撥動齒輪啟動執(zhí)行。是什么啟動這一連串的操作呢?也許你已經(jīng)想到了啟動的原始動力就是結(jié)束操作(Terminal Operation),一旦調(diào)用某個結(jié)束操作,就會觸發(fā)整個流水線的執(zhí)行。
結(jié)束操作之后不能再有別的操作,所以結(jié)束操作不會創(chuàng)建新的流水線階段(Stage),直觀的說就是流水線的鏈表不會在往后延伸了。結(jié)束操作會創(chuàng)建一個包裝了自己操作的Sink,這也是流水線中最后一個Sink,這個Sink只需要處理數(shù)據(jù)而不需要將結(jié)果傳遞給下游的Sink(因為沒有下游)。對于Sink的[處理->轉(zhuǎn)發(fā)]模型,結(jié)束操作的Sink就是調(diào)用鏈的出口。
我們再來考察一下上游的Sink是如何找到下游Sink的。一種可選的方案是在PipelineHelper中設(shè)置一個Sink字段,在流水線中找到下游Stage并訪問Sink字段即可。但Stream類庫的設(shè)計者沒有這么做,而是設(shè)置了一個Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來得到Sink,該方法的作用是返回一個新的包含了當(dāng)前Stage代表的操作以及能夠?qū)⒔Y(jié)果傳遞給downstream的Sink對象。為什么要產(chǎn)生一個新對象而不是返回一個Sink字段?這是因為使用opWrapSink()可以將當(dāng)前操作與下游Sink(上文中的downstream參數(shù))結(jié)合成新Sink。試想只要從流水線的最后一個Stage開始,不斷調(diào)用上一個Stage的opWrapSink()方法直到最開始(不包括stage0,因為stage0代表數(shù)據(jù)源,不包含操作),就可以得到一個代表了流水線上所有操作的Sink,用代碼表示就是這樣:
code6AbstractPipeline.wrapSink
現(xiàn)在流水線上從開始到結(jié)束的所有的操作都被包裝到了一個Sink里,執(zhí)行這個Sink就相當(dāng)于執(zhí)行整個流水線,執(zhí)行Sink的代碼如下:
code7AbstractPipeline.copyInto
上述代碼首先調(diào)用wrappedSink.begin()方法告訴Sink數(shù)據(jù)即將到來,然后調(diào)用spliterator.forEachRemaining()方法對數(shù)據(jù)進行迭代,最后調(diào)用wrappedSink.end()方法通知Sink數(shù)據(jù)處理結(jié)束。邏輯如此清晰。
以上就是解析Java8 Stream原理的詳細內(nèi)容,更多關(guān)于Java8 Stream原理的資料請關(guān)注腳本之家其它相關(guān)文章!
- 一篇文章帶你了解Java Stream流
- Java之OutputStreamWriter流案例詳解
- 如何用Java Stream寫出既高雅又裝*的代碼
- 手把手帶你了解Java-Stream流方法學(xué)習(xí)及總結(jié)
- Java FileInputStream與FileOutputStream使用詳解
- Java inputstream和outputstream使用詳解
- JAVA 筆記 ClassLoader.getResourceAsStream() 與 Class.getResourceAsStream()的區(qū)別
- 基于Java8并行流(parallelStream)的注意點
- Java8的Stream()與ParallelStream()的區(qū)別說明
- java8 使用stream排序空字段排在前面或后面
- 總結(jié)一下關(guān)于在Java8中使用stream流踩過的一些坑
- Java8 中使用Stream 讓List 轉(zhuǎn) Map使用問題小結(jié)
- java8 Stream list to Map key 重復(fù) value合并到Collectio的操作
- 關(guān)于JAVA8的 Stream學(xué)習(xí)
相關(guān)文章
SpringBoot整合Shiro實現(xiàn)權(quán)限控制的代碼實現(xiàn)
Apache Shiro是一個強大且易用的Java安全框架,執(zhí)行身份驗證、授權(quán)、密碼和會話管理,今天通過本文給大家介紹SpringBoot整合Shiro實現(xiàn)權(quán)限控制的方法,感興趣的朋友一起看看吧2021-07-07實例化JFileChooser對象報空指針異常問題的解決辦法
今天小編就為大家分享一篇關(guān)于實例化JFileChooser對象報空指針異常問題的解決辦法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02spring整合redis緩存并以注解(@Cacheable、@CachePut、@CacheEvict)形式使用
本篇文章主要介紹了spring整合redis緩存并以注解(@Cacheable、@CachePut、@CacheEvict)形式使用,具有一定的參考價值,有興趣的可以了解一下。2017-04-04Mybatis中的resultType和resultMap使用
這篇文章主要介紹了Mybatis中的resultType和resultMap使用,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-09-09集合框架及背后的數(shù)據(jù)結(jié)構(gòu)
本文主要介紹了Java的集合框架Java?Collection?Framework,接口interface以及背后的數(shù)據(jù)結(jié)構(gòu),感興趣的同學(xué)可以閱讀參考2023-03-03