Collection如何轉成stream以及Spliterator對其操作的實現

dust1發表於2018-10-07

Collection如何轉成stream

在java 1.8中,Collection新增了一個default方法stream(),他可以將集合轉換成流,那麼這節我將會深入原始碼看看具體過程是如何。

Collection中的操作

首先檢視Collection中的stream方法

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
複製程式碼

該方法呼叫了StreamSupport.stream方法,在前面我分析了StreamSupport的這個方法的執行步驟,該方法接收一個Spliterator引數以及是否是併發的判斷,主要的資料儲存在Spliterator中。此時我們再看看spliterator()方法

@Override
default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}
複製程式碼

這是一個重寫方法,它的父類方法如下:

default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
複製程式碼

該方法呼叫Spliterators.spliteratorUnknownSize()方法,並傳入一個迭代器和特徵值,而Collection中重寫的方法呼叫的是Spliterators.spliterator(),傳遞的是一個Collection物件和特徵值.
我們直接看Spliterator.spliterator()方法

public static <T> Spliterator<T> spliterator(Collection<? extends T> c,
                                                 int characteristics) {
    return new IteratorSpliterator<>(Objects.requireNonNull(c),
                                     characteristics);
}
複製程式碼

它先判斷傳入的集合是否為空,然後返回一個IteratorSpliterator物件,接著我們看看這個物件的構造方法

public IteratorSpliterator(Collection<? extends T> collection, int characteristics) {
    this.collection = collection;
    this.it = null;
    this.characteristics = (characteristics & Spliterator.CONCURRENT) == 0
                           ? characteristics | Spliterator.SIZED | Spliterator.SUBSIZED
                           : characteristics;
}
複製程式碼

該構造方法的作用是根據給定的集合建立一個spliterator。該方法位於Spliterators類下面,根據java8對於這些類-介面的設計模式,該類主要用於對Spliterator的具體靜態實現。因此我們直接看IteratorSpliterator類的繼承關係

Collection如何轉成stream以及Spliterator對其操作的實現

文件上對於它的描述為:

使用給定迭代器進行元素操作的Spliterator。 同時實現分裂器trySplit()以允許有限的並行性。

可見,該類主要用於迭代器生成Spliterator,並且允許一定的併發性。 既然生成了Spliterator物件,那麼就可以直接呼叫StreamSupport.stream(spliterator: Spliterator, parallel : boolean)方法建立Stream了。

Spliterator對其操作的實現

這節主要介紹上一節的迭代器Spliterator的具體實現,讓我們看看jdk作者們是如何對有序的集合進行併發操作的。

首先我們看看IteratorSpliterator新定義的引數

static final int BATCH_UNIT = 1 << 10;  // batch array size increment
static final int MAX_BATCH = 1 << 25;  // max batch array size;
private final Collection<? extends T> collection; // null OK
private Iterator<? extends T> it;
private final int characteristics;
private long est;             // size estimate
private int batch;            // batch size for splits
複製程式碼

其中我們需要關注的是batch引數,因為他就是涉及到如何拆分,該引數的作用是確定拆分的大小。而這個characteristics便是它的特徵值,這個值直接關係到生成stream時候限定的各個階段能進行的操作。 對特徵值的計算如下:

characteristics = (characteristics & Spliterator.CONCURRENT) == 0
                                   ? characteristics | Spliterator.SIZED | Spliterator.SUBSIZED
                                   : characteristics;
複製程式碼

其中的Spliterator.CONCURRENT參數列示可以在沒有外部同步的情況下由多個執行緒安全地同時修改元素源(允許新增,替換和/或刪除)的特徵值。這裡的判斷為:如果給定的特徵值不允許並行,那麼該特徵值允許按順序遍歷(Spliterator.SIZED)並且該資料由Spliterator產生(Spliterator.SUBSIZED),否則直接保持原來的特徵值。

關於特徵值的計算我之前一直沒懂為什麼都是用 | 和 & 這兩個運算子號,直到現在我才發現,這是為了在有限的位數中儘可能地多儲存資訊。而|是為了增加資訊,&是為了判斷是否存在該資訊。比如:

有一個List,對於它的操作由一組特徵值限定。

  • 0b001 - 允許增加
  • 0b010 - 允許刪除
  • 0b100 - 允許修改

當這個List的特徵值為0b011時:
    0b001 & 0b011的結果為1,0b011 & 0b010的結果為1,0b011 & 0b100的結果為0,則該List允許增加、刪除,但是不允許修改。
那麼如何讓他允許修改呢?
    只要將特徵值修改為0b011 | 0b100即可,該結果為7轉換成二進位制則為0b111.

接著檢視重頭戲,IteratorSpliterator對trySplit的實現:

@Override
public Spliterator<T> trySplit() {
    Iterator<? extends T> i;
    long s;
    if ((i = it) == null) {
        i = it = collection.iterator();
        s = est = (long) collection.size();
    }
    else
        s = est;
    if (s > 1 && i.hasNext()) {
        int n = batch + BATCH_UNIT;
        if (n > s)
            n = (int) s;
        if (n > MAX_BATCH)
            n = MAX_BATCH;
        Object[] a = new Object[n];
        int j = 0;
        do { a[j] = i.next(); } while (++j < n && i.hasNext());
        batch = j;
        if (est != Long.MAX_VALUE)
            est -= j;
        return new ArraySpliterator<>(a, 0, j, characteristics);
    }
    return null;
}
複製程式碼

這裡的操作我們對應上面的構造方法,我們的流程是使用集合來構造Spliterator的因此,表示迭代器的it為null。因此會執行第一個條件判斷

if ((i = it) == null) {
    i = it = collection.iterator();
    s = est = (long) collection.size();
}
複製程式碼

此時會將集合中的迭代器賦值給i和it,集合中的元素個數賦值給s和est。否則就將迭代器中的元素個數賦值給s。

當元素的個數大於1的時候int n的值為拆分的批量大小與批量大小陣列增量(1 << 10 = 2 ^ 10 = 1024)的和。如果該值大於元素的個數,則將n賦值為元素個數,如果該值大於最大批量陣列的大小(1 << 25 = 2 ^ 25 = 0x2000000)則將n賦值為最大批量陣列的大小。然後建立Object[]陣列,陣列大小為n,將迭代器中的元素新增進陣列,然後將拆分的批量大小(batch)賦值為迴圈賦值次數,如果最開始迭代器中的元素個數不等於Long的最大值(0x7fffffffffffffffL),則est減去迴圈次數,即est為剩下的元素的個數。將賦值後的Object[]作為引數建立ArraySpliterator物件。

官方給這段程式碼的註釋為:

分成算術增加批量大小的陣列。 如果每個元素的Consumer操作比將它們轉移到陣列中更昂貴,那麼這隻會提高並行效能。 在分割大小中使用算術級數提供了開銷與並行性邊界,這些邊界不會特別有利於或懲罰輕量級與重量級元素操作的情況,跨越#elements與#cores的組合,無論是否已知。 我們生成O(sqrt(#elements))分割,允許O(sqrt(#cores))潛在的加速。

而對於元素個數的邊界為批量大小陣列的增量BATCH_UNIT(1 << 10 = 1024).即將1024個元素分成一組進行併發操作.

相關文章