java8原始碼學習-java.util.stream:我Stream誕生啦

dust1發表於2018-10-02

這會是一系列的文章,用來記錄我學習jdk8-Stream原始碼的學習筆記。我會通過直接檢視jdk原始碼以及jdk8文件來進行學習,其中會有我自己的理解,可能沒有用到專業術語或者有錯誤,歡迎捉蟲。

jdk8-stream簡介

根據oracle官方對jdk8新特性的介紹我們可以看到關於java.util.stream的簡介:

  • 新的 java.util.stream 包中的類提供了一個 Stream API,支援對元素流進行函式式操作。Stream API 整合在 Collections API 中,可以對集合進行批量操作,例如順序或並行的 map-reduce 轉換。 來自:oracle-jdk8

java.util.stream目錄結構

目錄結構如下:

java8原始碼學習-java.util.stream:我Stream誕生啦

官方文件對他的目錄結構如下:

java8原始碼學習-java.util.stream:我Stream誕生啦

從結構我們可以發現,java.util.stream下面的主要類就是StreamSupport,我會從這個類開始一步一步學習。

開始學習

讓我們先從jdk文件看起,根據文件對StreamSupport的介紹

  • public final class StreamSupport extends Object
  • Low-level utility methods for creating and manipulating streams.
  • This class is mostly for library writers presenting stream views of data structures; most static stream methods intended for end users are in the various Stream classes.

它除了繼承Object類之外沒有繼承或實現任何類。 主要用於建立和操作流的低階實用程式方法。提供資料結構的流檢視。 針對終端使用者的大多數靜態流方法都在各種Stream類中。

從介紹我們發現,StreamSupport主要用於建立和操作流的低階程式方法。而針對使用者的大部門流的操作方法都在各種Stream類中。我們可以發現在文件中顯示的類除了StreamSupport只有一個Collectors類,那麼這裡我猜測流的大部分操作方法都用default修飾並在interface中實現。這裡我先不直接看IntStream之類的類,還是繼續檢視StreamSupport。

建構函式部分:

private StreamSupport() {}
複製程式碼

這裡很有意思,它的建構函式用private修飾,當用private修飾建構函式的時候改函式便無法被new例項化。關於這方面我查詢了《thinking in java》的相關內容,他對於這部分的介紹如下:

可能想控制如何建立物件,並阻止別人訪問某個特定構造器(或全部構造器)。他還有另一個效果:既然預設構造器是唯一的構造器,並且它是private的,那麼它將阻礙對此類的繼承。

而原始碼上的註釋也表明了這點:

Suppresses default constructor, ensuring non-instantiability.

<T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel)

文件的介紹如下:

Creates a new sequential or parallel Stream from a Spliterator.

從Spliterator建立新的順序或並行流。

原始碼:

public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
        Objects.requireNonNull(spliterator);
        return new ReferencePipeline.Head<>(spliterator,
                                            StreamOpFlag.fromCharacteristics(spliterator),
                                            parallel);
    }
複製程式碼

對於它的引數,通過文件可以獲知:

spliterator - a Spliterator describing the stream elements
parallel - if true then the returned stream is a parallel stream; if false the returned stream is a sequential stream.

一個是描述流的元素,一個是判斷是否是並行流

他會先進行null檢查

Objects.requireNonNull(spliterator);
複製程式碼

該方法是Objects類的靜態方法用於判斷傳入引數是否為空

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }
複製程式碼

然後返回一個ReferencePipeline.Head物件引用。 檢視這個類的構造器:

Head(Spliterator<?> source,
             int sourceFlags, boolean parallel) {
            super(source, sourceFlags, parallel);
        }
複製程式碼

該構造器接收三個引數:

  • source - 描述流的源
  • sourceFlags - 流源的原標誌
  • parallel - 是否為並行流

而這個類繼承自ReferencePipeline<E_IN, E_OUT>

static class Head<E_IN, E_OUT> extends ReferencePipeline<E_IN, E_OUT> {
    /** 程式碼省略 */
}
複製程式碼

它的連個範型和我平時接觸的T、E完全不一樣,通過檢視原始碼我發現這樣的命名是為了更方便理解兩個範型元素的作用

  • @param <E_IN>上游源中的元素型別
  • @param <E_OUT>此階段生成的元素型別

也就是說它是接收一個源再返回一個源

那麼我們繼續通過super檢視父類的構造器:

ReferencePipeline(Spliterator<?> source,
                      int sourceFlags, boolean parallel) {
        super(source, sourceFlags, parallel);
    }
複製程式碼

emmmm,這個父類ReferencePipeline的命名很有意思:"參考管道",我明明想要建立流,結果這給我命名了一個Pipeline.是不是說流就是一種特殊的管道呢?這裡我保持疑問,因為現在還不知道流如何誕生。

ReferencePipeline這個類由繼承了AbstractPipeline<P_IN, P_OUT, Stream<P_OUT>>以及實現了Stream<P_OUT>。這裡我還是先不看它的繼承結構,繼續通過super檢視父類構造器:

 AbstractPipeline(Spliterator<?> source,
                     int sourceFlags, boolean parallel) {
        this.previousStage = null;
        this.sourceSpliterator = source;
        this.sourceStage = this;
        this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
        this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
        this.depth = 0;
        this.parallel = parallel;
    }
複製程式碼

這裡有很多引數,我一個一個找出來:

  • AbstractPipeline previousStage - The "upstream" pipeline, or null if this is the source stage.
  • 上游管道,如果是原階段(也就是第一次建立流)則為null
  • Spliterator<?> sourceSpliterator - The source spliterator. Only valid for the head pipeline. Before the pipeline is consumed if non-null then {@code sourceSupplier} must be null. After the pipeline is consumed if non-null then is set to null.
  • 源分裂器。僅對頭管道有效。如果非null,則在使用管道之前,{@code sourceSupplier}必須為null並且在在使用管道之後將其設定為null。
  • AbstractPipeline sourceStage - Backlink to the head of the pipeline chain (self if this is the source stage).
  • 反向連結到管道鏈的頭部(如果這是源階段,則為它本身)。
  • int sourceOrOpFlags - The operation flags for the intermediate operation represented by this pipeline object.
  • 此管道物件中表示的中間操作的操作標誌。
  • int combinedFlags - The combined source and operation flags for the source and all operations up to and including the operation represented by this pipeline object. Valid at the point of pipeline preparation for evaluation.
  • 源以及所有操作的組合源的操作標誌,包括此管道物件表示的操作。在評估管道準備時有效。
  • int depth - The number of intermediate operations between this pipeline object and the stream source if sequential, or the previous stateful if parallel. Valid at the point of pipeline preparation for evaluation.
  • 此管道物件與流源(如果是順序)之間的中間運算元,或之前的有狀態(如果並行)。在評估管道準備時有效。
  • boolean parallel - True if pipeline is parallel, otherwise the pipeline is sequential; only valid for the source stage
  • 如果管道是並行的,則為真,否則管道是順序的;僅對源階段有效.

在我們呼叫的時候sourceSupplier沒有沒賦值為null,則源分裂器被賦值為source源流. 由於我是第一次建立,則它的上游管道為null同時反向連結管道的頭部也為它本身,用this賦值;而對源幾次所有操作的組合源的操作標誌為"源的原標誌 & 源流標誌的位掩碼。"關於流源標誌的位掩碼我檢視它的定義為:

static final int STREAM_MASK = createMask(Type.STREAM);
複製程式碼

他根據Type.STREAM由createMask生成,該方法如下:

private static int createMask(Type t) {
        int mask = 0;
        for (StreamOpFlag flag : StreamOpFlag.values()) {
            mask |= flag.maskTable.get(t) << flag.bitPosition;
        }
        return mask;
    }
複製程式碼

他會通過遍歷獲取StreamOpFlag型別的物件然後呼叫它的maskTable.get(Type)方法, 我先檢視StreamOpFlag.values()看看遍歷的物件有哪些

enum StreamOpFlag {
    DISTINCT(0, set(Type.SPLITERATOR).set(Type.STREAM).setAndClear(Type.OP)),
    SORTED(1, set(Type.SPLITERATOR).set(Type.STREAM).setAndClear(Type.OP)),
    ORDERED(2, set(Type.SPLITERATOR).set(Type.STREAM).setAndClear(Type.OP).clear(Type.TERMINAL_OP).clear(Type.UPSTREAM_TERMINAL_OP)),
    SIZED(3, set(Type.SPLITERATOR).set(Type.STREAM).clear(Type.OP)),
    SHORT_CIRCUIT(12, set(Type.OP).set(Type.TERMINAL_OP));

    /** other code */
}
複製程式碼

原始碼中對於該類的註釋為:

Flags corresponding to characteristics of streams and operations. Flags are utilized by the stream framework to control, specialize or optimize computation. Stream flags may be used to describe characteristics of several different entities associated with streams: stream sources, intermediate operations, and terminal operations. Not all stream flags are meaningful for all entities; the following table summarizes which flags are meaningful in what contexts:

對應於流和操作的特徵的標誌。流框架使用標誌來控制,專門化或優化計算。 流標誌可用於描述與流相關聯的若干不同實體的特徵:流源、中間操作和終端操作。並非所有流標誌對所有實體都有意義;下表總結了哪些標誌在哪些上下文中有意義: 表1.0

java8原始碼學習-java.util.stream:我Stream誕生啦
在上表中,“PCI”表示“可以儲存,清除或注入”; “PC”表示“可以保留或清除”,“PI”表示“可以保留或注入”,“N”表示“無效”。

回到原始碼部分,我去其中一個來看

DISTINCT(0, set(Type.SPLITERATOR).set(Type.STREAM).setAndClear(Type.OP)),
複製程式碼

它傳入兩個引數,根據StreamOpFlag的構造器可以看到:

private StreamOpFlag(int position, MaskBuilder maskBuilder) {
        this.maskTable = maskBuilder.build();
        // Two bits per flag
        position *= 2;
        this.bitPosition = position;
        this.set = SET_BITS << position;
        this.clear = CLEAR_BITS << position;
        this.preserve = PRESERVE_BITS << position;
    }
複製程式碼

它接收一個位置特徵值和匹配的掩碼生成器。

每個特徵在位集中佔用2位以適應保留、清除和設定/注入資訊。 這適用於流標誌,中間/終端操作標誌以及組合的流和操作標誌。 即使前者每個特性僅需要1位資訊,但在組合標誌以對齊集合和注入位時更有效。

特徵屬於某些型別,具體的就是列舉的型別。型別的位掩碼按照下表構造:

表1.1

Type DISTINCT SORTED ORDERED SIZED SHORT_CIRCUIT
SPLITERATOR 01 01 01 01 00
STREAM 01 01 01 01 00
OP 11 11 11 10 01
TERMINAL_OP 00 00 10 00 01
UPSTREAM_TERMINAL_OP 00 00 10 00 00
  • 01 = set/inject 設定/注入
  • 10 = clear 清除
  • 11 = preserve 保留

Type列對應的是enum的列舉。那麼我們選取的該行程式碼的意思是:

位置特徵為0,匹配的掩碼生成器 01(SPLITERATOR, DISTINCT) 01(STREAM, DISTINCT) 11(OP, DISTINCT)

此時檢視MaskBuilder類


private static final int SET_BITS = 0b01;

private static final int CLEAR_BITS = 0b10;

private static final int PRESERVE_BITS = 0b11;
    
private static class MaskBuilder {
        final Map<Type, Integer> map;

        MaskBuilder(Map<Type, Integer> map) {
            this.map = map;
        }

        MaskBuilder mask(Type t, Integer i) {
            map.put(t, i);
            return this;
        }

        MaskBuilder set(Type t) {
            return mask(t, SET_BITS);
        }

        MaskBuilder clear(Type t) {
            return mask(t, CLEAR_BITS);
        }

        MaskBuilder setAndClear(Type t) {
            return mask(t, PRESERVE_BITS);
        }

        Map<Type, Integer> build() {
            for (Type t : Type.values()) {
                map.putIfAbsent(t, 0b00);
            }
            return map;
        }
    }
複製程式碼

該類通過set(),clear(), setAndClear() 這三個方法給特徵碼對應的型別建立相應的key-value結構。這個例子中MaskBuilder最終的結果是:

{
    Type.SPLITERATOR : 0b01,
    Type.STREAM : 0b01,
    Type.Op : 0b11
}
複製程式碼

最終呼叫建構函式得建構函式的引數如下:

{
    maskTable : {
        Type.SPLITERATOR : 0b01,
        Type.STREAM : 0b01,
        Type.Op : 0b11,
        TERMINAL_OP : 0b00,
        UPSTREAM_TERMINAL_OP : 0b00
    }, 
    position : position *= 2,
    bitPosition : position,
    set : 0b01 << position,
    clear : 0b01 << position,
    preserve : 0b11 << position
}
複製程式碼

此時回到createMask方法,它會對maskTable中的元素進行遍歷然後使用<<轉換符使得對應的掩碼左移position位然後用|=賦值給mask。這裡我找資料才理解|=運算:

a|=b的意思就是把a和b按位或然後賦值給a 按位或的意思就是先把a和b都換成2進位制,然後用或操作,相當於a=a|b 。這裡我直接舉例子:

0 |= 1 : 先轉換成二進位制:0000 |= 0001,那麼對應位數是0 - 1,此時 0 | 1 = 1,所以最終結果是0001;
1 |= 3 : 轉換成二進位制 : 0001 |= 0011,此時各個位數對應關係: 1 | 1 = 1, 0 | 1 = 1;最終結果位0011;
看來相關的基礎還是不怎麼熟練。

那麼createMask這個方法的最終作用就出來了:

它根據傳入的Type對應表1.1得到對應的流掩碼,再根據position對掩碼進行左移操作,並將得到的所有結果按位或。

最後我們回到sourceOrOpFlags的賦值:

獲取流的掩碼和源流的原標誌的按位與的結果

接著檢視

this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
複製程式碼

對於該引數的解釋在上面有

源以及所有操作的組合源的操作標誌,包括此管道物件表示的操作。在評估管道準備時有效。

那麼它是如何表示操作的呢?
我發現它的StreamOpFlag.INITIAL_OPS_VALUE;引數在原始碼中是兩個引數的按位或操作結果

int INITIAL_OPS_VALUE = FLAG_MASK_IS | FLAG_MASK_NOT;
複製程式碼

原始碼中對他的解釋為

要與管道中第一個流的流標誌組合的初始值。

而這兩個引數

    private static final int FLAG_MASK_IS = STREAM_MASK;
    private static final int FLAG_MASK_NOT = STREAM_MASK << 1;
複製程式碼

他們一個是設定流標誌的位置掩碼,一個是清理流標誌的位置掩碼。而這些操作的位置掩碼實際上都是依據STREAM_MASK這個源流標誌的位掩碼基礎上進行位移計算得到的。

最終對於combinedFlags的賦值:

此管道中表示中間操作的值左移一位後的按位否定與管道中第一個流標誌組合成的初始值的按位與結果

然後是depth變數:

此管道物件與流源之間的中間運算元(如果是順序的),或之前的有狀態if parallel.Valid在管道準備評估時。

此時對於AbstractPipeline的構造器初始化我已經有了一個大致的瞭解

對於流管道頭部的建構函式:它著重的值有兩個,一個是sourceOrOpFlags表示流的一系列中間操作。一個是combinedFlags表示流以及管道的所有操作(這裡我有點不理解: 流管道的所有操作與初始值之後的按位與結果這兩個具體是如何分工的?)

那麼到這裡對於呼叫到該方法之後的一系列操作我有了一個初步的認知

public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
        Objects.requireNonNull(spliterator);
        return new ReferencePipeline.Head<>(spliterator,
                                            StreamOpFlag.fromCharacteristics(spliterator),
                                            parallel);
    }
複製程式碼

等等,還差了一步:對於流源特徵位的獲取

StreamOpFlag.fromCharacteristics(spliterator),
複製程式碼

該方法如下:

static int fromCharacteristics(Spliterator<?> spliterator) {
        int characteristics = spliterator.characteristics();
        if ((characteristics & Spliterator.SORTED) != 0 && spliterator.getComparator() != null) {
            // Do not propagate the SORTED characteristic if it does not correspond
            // to a natural sort order
            return characteristics & SPLITERATOR_CHARACTERISTICS_MASK & ~Spliterator.SORTED;
        }
        else {
            return characteristics & SPLITERATOR_CHARACTERISTICS_MASK;
        }
    }
複製程式碼

這裡先獲取此Spliterator及其元素的一組特徵,並且與Spliterator.SORTED(0x00000004)進行按位與操作如果它的值不為0且這個Spliterator的來源不為null,則將Spliterator與分裂器的位掩碼還有遵循順序定義的特徵值的按位否定的結果進行按位與操作。else的話就不新增順序特徵值。對此,程式碼上的解釋為:

如果spliterator自然是{@code SORTED}(關聯的{@code Comparator}是{@code null}),那麼特性將被轉換為{@link #SORTED}標誌,否則特徵不會被轉換。
也就是說,特徵值最終都是要新增一個轉換標誌的。

十分簡短的總結

先前並沒有這樣一系列深挖原始碼,因此許多觀點都是想到什麼就寫什麼並且帶有我本人的一部分理解。而且是順著我自己的思路想到什麼寫什麼,因此會有些亂。歡迎大家來捉蟲。

對於上述涉及的類,我畫出了對應的uml圖,我用的是StarUML畫的,不知道為什麼類的實現關係變成了實線,因此我直接標註:

java8原始碼學習-java.util.stream:我Stream誕生啦

在Head的例項化過程中,最終引數的賦值為AbstractPipeline進行。

這裡我也標註出在例項話過程中涉及到的列舉型別的關係:

java8原始碼學習-java.util.stream:我Stream誕生啦

對於生成的位掩碼,它是按照表1.1生成的各個階段的操作。那麼對於流最終的生成,我個人的理解是:

建立管道物件,由於它是Stream,因此根據表1.1的STREAM獲取到它所有操作的位掩碼。

對於表1.0的操作,我這裡覺得是源spliterator生成新的Stream時候各個階段的操作,畢竟構造器中有兩個引數:previousStage和sourceStage,從這兩個引數的介紹我發現流應該是管道組成的環形連結串列形式。那麼它的資料存在哪裡以及平常所用的filter,limit操作是如何進行的呢?我下一章再講。

相關文章