從 Java 中的有限和無限流中獲取每個 N 個元素
1. 概述
Java Stream API 提供了各種方法來操作和處理元素序列。然而,如果我們只想處理流的一部分,例如每個第 N 個元素,那就不容易了。如果我們正在處理表示 CSV 檔案或資料庫表的原始資料流並且只想處理特定列,這可能會很有用。
我們將討論兩種類型的流:有限流和無限流。第一種情況可以透過將 Stream 轉換為允許索引的 List 來解決。另一方面,無限流需要不同的方法。在本教程中,我們將學習如何使用各種技術來應對這項挑戰。
2. 測試設置
我們將使用參數化測試來檢查我們解決方案的正確性。將有幾種情況,分別具有第 N 個元素和預期結果:
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Wednesday", "Saturday"), 3),
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Friday"), 5),
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"), 1)
現在,我們可以深入研究處理流中第 N 個元素的不同方法。
3.使用filter()
在第一種方法中,我們可以建立一個單獨的流,僅包含我們要處理的元素的索引。我們可以使用filter(Predicate)
來建立這樣的陣列:
void givenListSkipNthElementInListWithFilterTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
final List<String> actual = IntStream.range(0, sourceList.size())
.filter(s -> (s + 1) % n == 0)
.mapToObj(sourceList::get)
.collect(Collectors.toList());
assertEquals(expected, actual);
}
如果我們想要操作允許索引存取的資料結構(例如List.
所需的元素可以收集到一個新的List
中或使用forEach(Consumer).
4. 使用iterate()
這種方法與前一種方法類似,並且需要具有索引存取的資料結構。但是,我們不會過濾掉不需要的索引,而是只產生我們想要在開始時使用的索引:
void givenListSkipNthElementInListWithIterateTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
int limit = sourceList.size() / n;
final List<String> actual = IntStream.iterate(n - 1, i -> (i + n))
.limit(limit)
.mapToObj(sourceList::get)
.collect(Collectors.toList());
assertEquals(expected, actual);
}
在本例中,我們使用IntStream.iterate(int, IntUnaryOperator),
它允許我們建立一個包含n
步驟的整數序列。
5. 使用subList()
這種方法使用Stream.iterate
,與前一種類似,但它會建立一個Lists,
每個清單從nk-th
索引開始:
void givenListSkipNthElementInListWithSublistTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
int limit = sourceList.size() / n;
final List<String> actual = Stream.iterate(sourceList, s -> s.subList(n, s.size()))
.limit(limit)
.map(s -> s.get(n - 1))
.collect(Collectors.toList());
assertEquals(expected, actual);
}
我們應該取得每個清單的第一個元素來獲得所需的結果.
6. 使用自訂Collector
作為更先進和透明的解決方案,我們可以實現一個自訂Collector
,僅收集所需的元素:
class SkippingCollector {
private static final BinaryOperator<SkippingCollector> IGNORE_COMBINE = (a, b) -> a;
private final int skip;
private final List<String> list = new ArrayList<>();
private int currentIndex = 0;
private SkippingCollector(int skip) {
this.skip = skip;
}
private void accept(String item) {
int index = ++currentIndex % skip;
if (index == 0) {
list.add(item);
}
}
private List<String> getResult() {
return list;
}
public static Collector<String, SkippingCollector, List<String>> collector(int skip) {
return Collector.of(() -> new SkippingCollector(skip),
SkippingCollector::accept,
IGNORE_COMBINE,
SkippingCollector::getResult);
}
}
這種方法比較複雜,需要一些編碼。同時,該解決方案不允許並行化,並且即使在順序流上,技術上也可能會失敗,因為組合是一個實現細節,在未來的版本中可能會發生變化:
public static List<String> skipNthElementInStreamWithCollector(Stream<String> sourceStream, int n) {
return sourceStream.collect(SkippingCollector.collector(n));
}
然而,可以使用Spliterators
使這種方法適用於並行流,但它應該有一個很好的理由。
7. 簡單循環
以前的所有解決方案都可以工作,但總的來說,它們過於複雜,而且常常具有誤導性。解決問題的最佳方法通常是採用盡可能簡單的實作。這就是我們如何使用for
迴圈來實現相同的目的:
void givenListSkipNthElementInListWithForTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
List<String> result = new ArrayList<>();
for (int i = n - 1; i < sourceList.size(); i += n) {
result.add(sourceList.get(i));
}
final List<String> actual = result;
assertEquals(expected, actual);
}
然而,有時,我們需要直接使用Stream
,這不允許我們直接透過索引存取元素。在這種情況下,我們可以使用帶有while
循環的Iterator
:
void givenListSkipNthElementInStreamWithIteratorTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
List<String> result = new ArrayList<>();
final Iterator<String> iterator = input.iterator();
int count = 0;
while (iterator.hasNext()) {
if (count % n == n - 1) {
result.add(iterator.next());
} else {
iterator.next();
}
++count;
}
final List<String> actual = result;
assertEquals(expected, actual);
}
這些解決方案在解決相同問題的同時更清晰、更容易理解。
八、結論
Java Stream API 是一個強大的工具,有助於使程式碼更具聲明性和可讀性。此外,流可以透過利用參數化來實現更好的性能。然而,想要在任何地方使用串流可能並不是實現此 API 的最佳方式。
儘管在不理想的情況下應用串流操作的心理體操可能很有趣,但它也可能會產生“clever code.”
通常,最簡單的結構(例如循環)可以使用更少且更易於理解的程式碼來實現相同的結果。
與往常一樣,本文中使用的所有程式碼都可以在 GitHub 上找到。