用Java實現Stream流處理中的滑窗

banq發表於2018-09-04
簡單地說,滑窗演算法是一種移動固定大小的視窗(子列表)來遍歷資料結構的方法,主要是基於固定步驟的序列流資料。

如果我們想透過使用大小為3的視窗遍歷列表[1 2 3 4 5],我們透過視窗只能看到以下資料組:

[1 2 3]

[2 3 4]

[3 4 5]

.如果我們想要使用比集合大小更大的視窗遍歷相同的列表,我們甚至不會得到一個元素。

Java 10提供了一種Stream實現,支援順序和並行聚合操作的一系列元素:

 int sum = widgets.stream()
                      .filter(w -> w.getColor() == RED)
                      .mapToInt(w -> w.getWeight())
                      .sum();
<p class="indent">


下面談的是如何在這個流上使用滑窗演算法。

為了能夠建立自定義Stream,我們需要實現自定義Spliterator

在我們的例子中,我們需要能夠迭代Stream <T>序列資料,因此我們需要實現Spliterator介面並指定泛型型別引數:

public class SlidingWindowSpliterator<T> implements Spliterator<Stream<T>> {
// ...
}
<p class="indent">


有一堆方法需要實現:

public class SlidingWindowSpliterator<T> implements Spliterator<Stream<T>> {
//下面會實現
@Override
public boolean tryAdvance(Consumer<? super Stream<T>> action) {
return false;
}
//準備下面實現
@Override
public Spliterator<Stream<T>> trySplit() {
return null;
}
@Override
public long estimateSize() {
return 0;
}
//下面準備實現
@Override
public int characteristics() {
return 0;
}
}
<p class="indent">


我們還需要一些欄位來儲存緩衝元素、視窗大小引數、源集合的迭代器以及預先計算的大小估計(稍後我們將需要):

private final Queue<T> buffer;
private final Iterator<T> sourceIterator;
private final int windowSize;
private final int size;
<p class="indent">


在我們開始實現介面方法之前,我們需要能夠例項化我們的工具。

在這種情況下,我們將限制建構函式的可見性,並公開一個公共靜態工廠方法:

private SlidingWindowSpliterator(Collection<T> source, int windowSize) {
this.buffer = new ArrayDeque<>(windowSize);
this.sourceIterator = Objects.requireNonNull(source).iterator();
this.windowSize = windowSize;
this.size = calculateSize(source, windowSize);
}
<p class="indent">

公開的靜態方法:

static <T> Stream<Stream<T>> windowed(Collection<T> stream, int windowSize) {
return StreamSupport.stream(
new SlidingWindowSpliterator<>(stream, windowSize), false);
}
<p class="indent">


現在讓我們實現Spliterator方法中容易的部分。

實現 trySplit()時,我們預設使用文件中指定的值。幸運的是,計算大小很容易:

private static int calculateSize(Collection<?> source, int windowSize) {
return source.size() < windowSize
? 0
: source.size() - windowSize + 1;
}
@Override 
public Spliterator<Stream<T>> trySplit() { 
return null; 
} 
@Override 
public long estimateSize() { 
return size; 
}
<p class="indent">


在characteristics()中,我們指定:

ORDERED - 因為順序很重要
NONNULL - 因為元素永遠不會為null(儘管可以包含空值)
SIZED -因為大小是可以預見的

@Override
public int characteristics() {
return ORDERED | NONNULL | SIZED;
}
<p class="indent">


現在實現tryAdvance,這裡是關鍵部分 - 負責實際分組和迭代的方法。

首先,如果視窗小於1,則沒有任何內容可以迭代,以便我們可以立即返回:

@Override
public boolean tryAdvance(Consumer<? super Stream<T>> action) {
if (windowSize < 1) {
return false;
}
// ...
}
<p class="indent">


現在,要生成第一個子列表,我們需要開始迭代並填充緩衝區:

while (sourceIterator.hasNext()) {
buffer.add(sourceIterator.next());
// ...
}
<p class="indent">


填充緩衝區後,我們可以排程整個組,並從緩衝區中丟棄最舊的元素。

這裡有一個關鍵部分,可能會試圖將buffer.stream()傳遞給accept()方法,這是一個巨大的錯誤 - Streams惰性地繫結到底層集合,這意味著如果源更改,Stream也會更改。

為了避免這個問題並將我們的組與內部緩衝區表示分離,我們需要在建立每個Stream例項之前對緩衝區的當前狀態進行快照。我們將使用陣列支援Stream例項,以使它們儘可能輕量級。

由於Java不支援通用陣列,我們需要做一些醜陋的轉換:

if (buffer.size() == windowSize) {
action.accept(Arrays.stream((T[]) buffer.toArray(new Object[0])));
buffer.poll();
return sourceIterator.hasNext();
}
<p class="indent">


...瞧,我們準備好使用它:

windowed(List.of(1,2,3,4,5), 3)
.map(group -> group.collect(toList()))
.forEach(System.out::println);
<p class="indent">


滑窗程式碼編製成功,執行結果如下:

// result
<p class="indent">[1, 2, 3]
<p class="indent">[2, 3, 4]
<p class="indent">[3, 4, 5]
<p class="indent">


github原始碼

用Java實現滑動視窗流/ Spliterator - {4Comprehension}

相關文章