深入理解 Stream 之原理剖析

我是碼哥,可以叫我靚仔。

JDK1.6 和 JDK1.8 是主流的兩個大版本,目前市場上用的最多最多的依然是 JDK1.8。所以,我們有必要聊一聊 Java8 的一些新特性。

今天我們先來聊聊深入理解 Stream 之原理剖析

Stream 操作分類

Stream 中的操作可以分爲兩大類:中間操作與結束操作。

中間操作只會進行操作記錄,只有結束操作纔會觸發實際的計算,可以理解爲懶加載,這也是 Stream 在操作大對象迭代計算的時候如此高效的原因之一。

中間操作分爲有狀態操作與無狀態操作,無狀態是指元素的處理不受之前元素的影響,有狀態是指該操作只有拿到所有元素之後才能繼續下去。這也比較好理解,比如有狀態的 distinct() 去重方法,你說他能不關心其他值嗎?當然不能,他必須拿到所有元素才知道當前迭代的元素是否被重複。

結束操作可以分爲短路與非短路操作,這個應該很好理解,短路是指遇到某些符合條件的元素就可以得到最終結果;而非短路是指必須處理所有元素才能得到最終結果。

之所以要進行如此精細的劃分,是因爲底層對每一種情況的處理方式不同。

Stream 結構分析

讓我們先簡單看看下面一段代碼:

        List<String> list = new ArrayList<>();

        // 獲取stream1
        Stream<String> stream1 = list.stream();

        // stream1通過filter後得到stream2
        Stream<String> stream2 = stream1.filter("lige"::equals);

        // stream1與stream2是同一個對象嗎?
        System.out.println("stream1.equals(stream2) = " + stream1.equals(stream2));
        System.out.println("stream1.classTypeName = " + stream1.getClass().getTypeName());
        System.out.println("stream2.classTypeName = " + stream2.getClass().getTypeName());

        // 結果
        // stream1.equals(stream2) = false
        // stream1.classTypeName = java.util.stream.ReferencePipeline$Head
        // stream1.classTypeName = java.util.stream.ReferencePipeline$2

很明顯,stream1 與 stream2 不是同一個對象,並且他們不是同一個實現類。stream1 的實現類爲 ReferencePipeline$Head,而 stream2 的實現類爲一個匿名內部類,讓我們進步一分析其源碼,所謂源碼之下,無所遁形。

讓我們再看看 stream2:

通過分析我們可以發現,stream2 的實現類是 StatelessOp,所以就形成了這樣一個結構。

每一次中間操作都會生成一個新的 Stream,如果是無狀態操作則實現類是 StatelessOp,如果是有狀態操作則實現類是 StatefulOp。

讓我們再來看一下他們之間的繼承關係。

再聊核心 Sink

實際上 Stream API 內部實現的的本質,就是如何重載 Sink 的這四個接口方法。

我還是從一個示例開始:

List<String> list = new ArrayList<>();
list.add("zhangsan");
list.add("ligeligeligeligeligeligeligeligeligelige");
list.add("lisilisilisilisilisilisilisilisi");
list.add("wangwu");
list.add("ligejishuligejishuligejishuligejishuligejishuligejishuligejishu");

List<String> resultList = list.stream()
        .filter(it -> it.contains("li"))// 1. 只要包含li的數據
        .filter(it -> it.contains("lige"))// 2. 只要包含lige的數據
        .map(String::toUpperCase)// 3. 對符合的數據作進一步加工,轉換大寫
        .map(String::toLowerCase)// 4. 對符合的數據作進一步加工,轉換小寫
        .collect(Collectors.toList());

resultList.forEach(System.out::println);

不管是 filter 方法,還是 map 方法,還是其他的方法,我們進入到源碼層面,返回了一個 StatelessOp 對象或 StatefulOp 對象。

所以便產生了這樣一個結構:

但是和 Sink 有什麼關係呢?我們再反過來看 filter 或者 map 源碼:

直接返回一個匿名 StatelessOp 對象,實現 opWrapSink 方法,opWrapSink 方法是傳入一個 sink 對象,返回另一個 sink 對象。而新的 sink 對象擁有傳入 sink 對象的引用。

但是,這個代碼有什麼用?什麼時候觸發的呢?

彆着急,讓我們從 collect(Collectors.toList()) 方法開始一步一步深入研究。

這裏我們需要知道傳入 xx 方法的終端對象是 ReduceOp,並且這個 ReduceOp 對象在 makeSink 的時候返回了一個匿名內部類 ReducingSink 對象。

這裏的 makeSink 我們提到過,返回一個匿名內部類 ReducingSink 對象。

先執行 warpSink,再執行 copyInto。直白一點就是先對 Sink 進行包裝成鏈式 Sink,再遍歷 Sink 鏈進行 copy 到結果對象裏。這裏的兩個步驟都很核心。

先看 warpSink:

  1. 首次進入時,this 爲最後的 Stream 對象,從尾部向頭部遍歷

  2. 每次遍歷時,得到一個新的 Stream 對象,一般爲 StatelessOp 對象或 StatefulOp 對象

  3. 執行操作對象的 opWrapSink 方法,這就是匿名實現了。

  4. 在每一個 opWrapSink 實現方法中,傳入了上一個 sink,最終得到一個 sink 鏈表

最後,返回 Sink 鏈的頭節點,內部稱之爲包裝好的 sink,命名 wrapped,隨後,準備進行執行 begin,forEachRemaining,end 方法。

forEachRemaning 最終調用 accept 方法。

動畫理解 Stream 執行流程

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KVIwdcjYThMjn5-qvGNIBQ