淺析Java8 Stream原理
上一篇文章中大體的介紹了Stream的概念和基本API的使用,Stream用起來的確非常的爽。這一片文章將會講述Stream的底層實現原理。
操作符的分類
Stream中的操作可以分為兩大類:中間操作與結束操作,中間操作只是對操作進行了記錄,只有結束操作才會觸發實際的計算(即惰性求值),這也是Stream在迭代大集合時高效的原因之一。
中間操作又可以分為無狀態(Stateless)操作與有狀態(Stateful)操作,前者是指元素的處理不受之前元素的影響;後者是指該操作只有拿到所有元素之後才能繼續下去。
結束操作又可以分為短路與非短路操作,這個應該很好理解,前者是指遇到某些符合條件的元素就可以得到最終結果;而後者是指必須處理所有元素才能得到最終結果。
流水線的結構
Stream的主要介面類的關係如下圖:
- BaseStream規定了流的基本介面
- Stream中定義了map、filter、flatmap等使用者關注的常用操作;
- Int~ Long~ Double~是針對於基本型別的特化 方法與Stream中大致對應,當然也有一些差別
- BaseStream Stream IntStream LongStream DoubleStream 組建了Java的流體系根基
- PipelineHelper主要用於Stream執行過程中相關結構的構建ReferencePipeline和AbstractPipeline
- AbstractPipeline是流水線的核心抽象類,用於構建和管理流水線。它的實現類就是流水線的節點。
- Head、StatelessOp、StatefulOp為ReferencePipeline中的內部類,[Int | Long | Double]Pipeline 內部也都是定義了這三個內部類。
IntPipeline, LongPipeline, DoublePipeline這三個類專門為三種基本型別而定製的,Int、long、double進行了優化,主要用於頻繁的拆裝箱。三者跟ReferencePipeline是並列關係,
StatefulOp、StatelessOp分別對應有狀態和無狀態中間操作。,很多Stream操作會需要一個回撥函式(Lambda表示式),一個完整的操作是<資料來源,操作,回撥函式>構成的三元組。
Stream中使用Stage的概念來描述一個完整的操作,將具有先後順序的各個Stage連到一起,就構成了整個流水線。
AbstractPipeline
前面說到AbstractPipeline是流水線的核心。AbstractPipeline中定義了三個個AbstractPipeline型別的變數:sourceStage(源階段),previousStage(上游pipeline,前一階段),nextStage(下一階段)。
/**
* Backlink to the head of the pipeline chain (self if this is the source stage).
*/
private final AbstractPipeline sourceStage;
/**
* The "upstream" pipeline, or null if this is the source stage.
*/
private final AbstractPipeline previousStage;
/**
* The next stage in the pipeline, or null if this is the last stage.
* Effectively final at the point of linking to the next pipeline.
*/
private AbstractPipeline nextStage;
它的直接實現類為ReferencePipeline,而Head 、StatefulOp 、StatelessOp又繼承了ReferencePipeline類。因此Head StatefulOp StatelessOp 他們本身也是AbstractPipeline型別的。
每一個stage就是一個AbstractPipeline的例項,注意已開始筆者和Netty中的Pipelie做以類比,其實根本不是一回事。這裡的每一個pipeline都是一個節點。
Head用於表示第一個Stage,也就是source stage,呼叫諸如Collection.stream()方法產生的Stage,很顯然這個Stage裡不包含任何操作;StatelessOp和StatefulOp分別表示無狀態和有狀態的Stage,對應於無狀態和有狀態的中間操作。
注意:終結操作不會新增節點。
Collection.stream()方法得到Head也就是stage0,緊接著呼叫一系列的中間操作,不斷產生新的Stream。這些Stream物件以雙向連結串列的形式組織在一起,構成整個流水線。
由於每個Stage都記錄了前一個Stage和本次的操作以及回撥函式,依靠這種結構就能建立起對資料來源的所有操作。
下面分析Head節點的構建。
Stream的生成原始碼分析
不管是Collection中呼叫StreamSupport.stream()還是Stream的of方法 都是呼叫了StreamSupport.stream方法。以Collecton.stream()為例
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
這是一個介面方法的預設實現,第一個引數是獲取一個 Spliterator的例項,它表示從資料來源中獲取元素的方式。相當於升級版的Iterator。第二個引數是是否並行。
繼續進入StreamSupport類中:
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
看到這裡和前面說的pipeline有點聯絡了,此處構造Head節點。我們看一下Head它的建構函式鏈:
Head(Spliterator<?> source,
int sourceFlags, boolean parallel) {
super(source, sourceFlags, parallel);//source - 描述流的源 sourceFlags - 流的來原標誌 parallel - 是否為並行流
}
ReferencePipeline(Spliterator<?> source,
int sourceFlags, boolean parallel) {
super(source, sourceFlags, parallel);
}
AbstractPipeline(Spliterator<?> source,
int sourceFlags, boolean parallel) {
this.previousStage = null; // 上游管道,第一次建立流則為null
this.sourceSpliterator = source; // 源分裂器。僅對頭管道有效。
this.sourceStage = this; // 反向連結到管道鏈的頭部(如果這是源階段,則為它本身)
this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK; //此管道物件中表示的中間操作的操作標誌。
// The following is an optimization of:
//源以及所有操作的組合源的操作標誌,包括此管道物件表示的操作。在評估管道準備時有效。
this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
this.depth = 0; //此管道物件與流源(如果是順序)之間的中間運算元,或之前的有狀態(如果並行)。在評估管道準備時有效。
this.parallel = parallel;//如果管道是並行的,則為真,否則管道是順序的;僅對源階段有效.
}
第一個建構函式對應的類為:static class Head<E_IN, E_OUT> extends ReferencePipeline<E_IN, E_OUT>
,
注意這一有一個細節:範型的命名,<E_IN>上游源中的元素型別 <E_OUT>此階段生成的元素型別
ReferencePipeline這個類由繼承了AbstractPipeline<P_IN, P_OUT, Stream<P_OUT>>
,同時實現了Stream<P_OUT>的各種操作符。
至此頭結點已經構造完成。
新增中間操作
看看filter的程式碼:
@Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
@Override
public void begin(long size) {
downstream.begin(-1);
}
@Override
public void accept(P_OUT u) {
if (predicate.test(u))
downstream.accept(u);
}
};
}
};
}
可以看到返回了一個無狀態stage,也是一個AbstractPipeline、stream,即是流水線的一個階段。同時還實現了AbstractPipeline定義的opWrapSink方法。
看到這裡似乎有些困惑,一片Override方法到底什麼意思?
Sink
Stream中將操作抽象化為stage 每個stage 也就是一個AbstractPipeline,每個stage 相當於一個雙向連結串列的節點 ,每個節點都儲存Head然後儲存著上一個和下一個節點。這個雙向連結串列就構成了整個流水線。
但是似乎有一個最重要的東西沒有提到,那麼就是每一個階段的操作。如何將多個操作疊加到一起呢?
你可能會覺得這很簡單,只需要從流水線的head開始依次執行每一步的操作(包括回撥函式)就行了。
這聽起來似乎是可行的,但是你忽略了前面的Stage並不知道後面Stage到底執行了哪種操作,以及回撥函式是哪種形式。換句話說,只有當前Stage本身才知道該如何執行自己包含的動作。這就需要有某種協議來協調相鄰Stage之間的呼叫關係。這就是Sink介面存在的意義。
建立的Sink.ChainedReference類構造方法如下:
public ChainedReference(Sink<? super E_OUT> downstream) {
this.downstream = Objects.requireNonNull(downstream);
}
Sink介面相當於對操作(我們實現的函式式介面)封裝了一層,每一個階段只需要呼叫自己的Sink的accept方法,accept內部只要呼叫下一個階段的accept
不需要知道下一個極端的操作型別是什麼。
當然Sink新增進行一些擴充套件功能,比如:begin表示開始遍歷元素前的方法,相當於AOP。end表示元素遍歷結束之後,cancellationRequested表示是否可以結束操作,可以讓短路操作儘早結束。
實際上Stream 操作符內部實現的的本質,就是實現Sink的這四個介面方法。
Sink介面的方法幾乎都是按照這種[處理->轉發]的模型實現,如上面的accept
1. 使用當前Sink包裝的回撥函式處理u
2. 將處理結果傳遞給流水線下游的Sink
當新增了中間操作符之後的連結串列結構如下,Head中沒有任何操作,因此也沒有實現Sink。
在這裡產生一個疑問,重寫的opWrapSink什麼時候被呼叫?以及Sink的accept什麼時候被呼叫?正如圖中所示,我們只是重寫了opWrapSink方法,儲存在每一個節點中。每一個Sink都是獨立的,它的downstream還沒有賦值。
萬事俱備,只欠東風
何為東風?顯然是終結操作符,以forEach為例,實現在ReferencePipeline中:
@Override
public void forEach(Consumer<? super P_OUT> action) {
evaluate(ForEachOps.makeRef(action, false));
}
ForEachOps是使用者建立TerminalOp例項的工廠類。TerminalOp是終止操作最頂層的一個介面。TerminalOp介面的實現類有ForEachOp, ReduceOp,FindOp, MatchOp。
先看ForEachOps.makeRef()方法:
public static <T> TerminalOp<T, Void> makeRef(Consumer<? super T> action,
boolean ordered) {
Objects.requireNonNull(action);
return new ForEachOp.OfRef<>(action, ordered);
}
OfRef是引用流的預設實現類,這裡新建了一個OfRef的例項,構造方法如下:
OfRef(Consumer<? super T> consumer, boolean ordered) {
super(ordered);// 父類ForEachOp,參數列述遍歷是否有序,前面傳入的false
this.consumer = consumer;
}
將我們實現的Consumer函式式介面賦值給成員變數。回到evaluate方法:
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
assert getOutputShape() == terminalOp.inputShape();
if (linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
linkedOrConsumed = true;
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}
跟到序列流的實現,實現在ForEachOp中:
@Override
public <S> Void evaluateSequential(PipelineHelper<T> helper,
Spliterator<S> spliterator) {
return helper.wrapAndCopyInto(this, spliterator).get();
}
數PipelineHelper型別其實是AbstractPipeline的父類,而AbstractPipeline又是ReferencePipeline的父類。再跟進helper.wrapAndCopyInto方法,是現在AbstractPipeline中:
@Override
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
return sink;
}
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink<P_IN>) sink;
}
可以看到通過ReferencePipeline的雙向連結串列,從最後一個操作(也就是終止操作)往前遍歷,將所有的操作都串聯起來,最終返回一個指向第一個操作的Sink引用。
這裡有一個細節問題:
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator)
方法的第一個入參為泛型,幾Sink的子類。對應的有Sink.ofLong、Sink.ofInt等。並且這個引數也屬於TerminalOp型別,說白了終結操作符最終也被包裝成了Sink型別,這一切都通了,最後一箇中間操作的downStream是終結操作符。
真的要執行了
回到copyInto方法,wrapSink反悔了Head後第一個中間操作的包裝Sink,繼續看copyInto的實現:
@Override
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
Objects.requireNonNull(wrappedSink);
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}
第一個引數我們知道Head後的節點,spliterator可以看作資料來源。邏輯分為短路操作和非短路操作,如果有短路操作就會執行下面的copyIntoWithCancel方法,否則指向上面的邏輯,這裡我們非常熟悉啊,begin、accept、end先後串聯執行。
短路流在執行遍歷的時候會呼叫Sink封裝的cancellationRequested方法,如果返回出就不會進行後面的操作,過程稍微比非短路複雜一點,但是原理大致相同。
相關文章
- 淺析nodejs中的stream(流)NodeJS
- Seata原理淺析
- 淺析DES原理
- 淺析Promise原理Promise
- AQS原理淺析AQS
- Webpack 原理淺析Web
- InheritedWidget原理淺析
- koa原理淺析
- BTrace 原理淺析
- markdown-it 原理淺析
- Java8 Lambda表示式、Optional類淺析Java
- mydumper使用及原理淺析
- Webpack相關原理淺析Web
- ArrayList底層原理淺析
- 淺析Hadoop基礎原理Hadoop
- 淺析volatile原理及其使用
- redux-saga 原理淺析Redux
- react-loadable原理淺析React
- Vuex 原理淺析筆記Vue筆記
- Array、Slice、Map原理淺析
- MySQL事務原理淺析MySql
- HashSet淺析原理學習
- Java8——Stream流Java
- 淺析瀑布流佈局原理
- TSDB - VictoriaMetrics 技術原理淺析
- 淺析Vite本地構建原理Vite
- Flutter 高效能原理淺析Flutter
- iOS應⽤簽名原理淺析iOS
- 淺析RunLoop原理及其應用OOP
- webpack系列--淺析webpack的原理Web
- Zookeeper ZAB協議原理淺析協議
- JavaScript模組化原理淺析JavaScript
- vue.js框架原理淺析Vue.js框架
- 執行緒池核心原理淺析執行緒
- 淺析MySQL 8.0直方圖原理MySql直方圖
- 淺析反向代理的原理與作用
- Flutter動畫實現原理淺析Flutter動畫
- 淺析MyBatis的動態代理原理MyBatis