如何中止迭代Java Stream forEach
1.概述
作為Java開發人員,我們經常編寫在一組元素上迭代並在每個元素上執行操作的代碼。 Java 8流庫及其forEach
方法使我們能夠以乾淨的聲明式方式編寫該代碼。
雖然這類似於循環,但我們缺少了break
語句來中止迭代。流可以很長,也可以是無限的,如果我們沒有理由繼續處理它,我們將要中斷它,而不是等待它的最後一個元素。
在本教程中,我們將研究一些機制,這些機制使我們可以在Stream.forEach
操作上模擬break
語句。
2. Java 9的Stream.takeWhile()
假設我們有一個String項流,並且只要它們的長度是奇數,我們就想處理它的元素。
讓我們嘗試一下Java 9 Stream.takeWhile
方法:
Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck")
.takeWhile(n -> n.length() % 2 != 0)
.forEach(System.out::println);
如果運行此命令,則會得到輸出:
cat
dog
讓我們使用for
循環和break
語句將其與普通Java中的等效代碼進行比較,以幫助我們了解其工作方式:
List<String> list = asList("cat", "dog", "elephant", "fox", "rabbit", "duck");
for (int i = 0; i < list.size(); i++) {
String item = list.get(i);
if (item.length() % 2 == 0) {
break;
}
System.out.println(item);
}
如我們所見, takeWhile
方法使我們能夠準確實現所需的功能。
但是,如果我們還沒有採用Java 9,該怎麼辦?我們如何使用Java 8實現類似的目標?
3.自定義Spliterator
讓我們創建一個自定義的Spliterator
,將其用作Stream.spliterator
的裝飾器。我們可以讓這個Spliterator
為我們執行break
。
首先,我們從流中獲取Spliterator
,然後使用CustomSpliterator
裝飾它,並提供Predicate
來控制break
操作。最後,我們將根據CustomSpliterator:
創建一個新流CustomSpliterator:
public static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<T> predicate) {
CustomSpliterator<T> customSpliterator = new CustomSpliterator<>(stream.spliterator(), predicate);
return StreamSupport.stream(customSpliterator, false);
}
讓我們看看如何創建CustomSpliterator
:
public class CustomSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
private Spliterator<T> splitr;
private Predicate<T> predicate;
private boolean isMatched = true;
public CustomSpliterator(Spliterator<T> splitr, Predicate<T> predicate) {
super(splitr.estimateSize(), 0);
this.splitr = splitr;
this.predicate = predicate;
}
@Override
public synchronized boolean tryAdvance(Consumer<? super T> consumer) {
boolean hadNext = splitr.tryAdvance(elem -> {
if (predicate.test(elem) && isMatched) {
consumer.accept(elem);
} else {
isMatched = false;
}
});
return hadNext && isMatched;
}
}
因此,讓我們看一下tryAdvance
方法。我們在這裡可以看到,自定義的Spliterator
處理了裝飾的Spliterator
的元素。只要我們的謂詞匹配並且初始流仍然具有元素,就完成處理。當任一條件變為false
,我們的Spliterator
“breaks”
,並且流操作結束。
讓我們測試我們的新輔助方法:
@Test
public void whenCustomTakeWhileIsCalled_ThenCorrectItemsAreReturned() {
Stream<String> initialStream =
Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck");
List<String> result =
CustomTakeWhile.takeWhile(initialStream, x -> x.length() % 2 != 0)
.collect(Collectors.toList());
assertEquals(asList("cat", "dog"), result);
}
如我們所見,流在滿足條件後停止。為了進行測試,我們將結果收集到一個列表中,但是我們也可以使用forEach
調用或Stream
任何其他函數。
4.自定義forEach
儘管為Stream
提供嵌入的break
機制可能是有用的,但僅關注forEach
操作可能會更簡單。
讓我們直接使用**Stream.spliterator
**而不使用裝飾器:
public class CustomForEach {
public static class Breaker {
private boolean shouldBreak = false;
public void stop() {
shouldBreak = true;
}
boolean get() {
return shouldBreak;
}
}
public static <T> void forEach(Stream<T> stream, BiConsumer<T, Breaker> consumer) {
Spliterator<T> spliterator = stream.spliterator();
boolean hadNext = true;
Breaker breaker = new Breaker();
while (hadNext && !breaker.get()) {
hadNext = spliterator.tryAdvance(elem -> {
consumer.accept(elem, breaker);
});
}
}
}
如我們所見,新的自定義forEach
方法調用BiConsumer
,該BiConsumer
為我們的代碼提供了下一個元素和可以用來停止流的breaker對象。
讓我們在單元測試中嘗試一下:
@Test
public void whenCustomForEachIsCalled_ThenCorrectItemsAreReturned() {
Stream<String> initialStream = Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck");
List<String> result = new ArrayList<>();
CustomForEach.forEach(initialStream, (elem, breaker) -> {
if (elem.length() % 2 == 0) {
breaker.stop();
} else {
result.add(elem);
}
});
assertEquals(asList("cat", "dog"), result);
}
5.結論
在本文中,我們研究了提供等同於流調用break
的方法。我們了解了Java 9的takeWhile
如何為我們解決了大多數問題,以及如何為Java 8提供該版本。
最後,我們研究了一種實用程序方法,該方法可以在對Stream
進行迭代時為我們提供相當於break
操作的功能。
與往常一樣,示例代碼可以在GitHub上找到。