將 Java 流過濾為 1 個且僅 1 個元素
一、概述
在本文中,我們將使用Collectors
中的兩種方法來檢索與給定元素流中的某個謂詞匹配的唯一元素。
對於這兩種方法,我們將根據以下標准定義兩種方法:
- get 方法期望有一個獨特的結果。否則,它會拋出
[Exception](https://baeldung-cn.com/java-exceptions)
- find 方法接受結果可能會丟失,如果存在則返回帶有值的
Optional
2. 使用歸約檢索唯一結果
Collectors.reducing
執行其輸入元素的歸約。為此,它應用指定為BinaryOperator
的函數。結果被描述為Optional
。因此我們可以定義我們的 find 方法。
在我們的例子中,如果過濾後有兩個或多個元素,我們只需要丟棄結果:
public static <T> Optional<T> findUniqueElementMatchingPredicate_WithReduction(Stream<T> elements, Predicate<T> predicate) {
return elements.filter(predicate)
.collect(Collectors.reducing((a, b) -> null));
}
要編寫 get 方法,我們需要進行以下更改:
- 如果我們檢測到兩個元素,我們可以直接拋出它們而不是返回 null
- 最後,我們需要獲取
Optional
的值:如果它是空的,我們也想拋出
此外,在這種情況下,我們可以直接對**Stream**
應用歸約操作:
public static <T> T getUniqueElementMatchingPredicate_WithReduction(Stream<T> elements, Predicate<T> predicate) {
return elements.filter(predicate)
.reduce((a, b) -> {
throw new IllegalStateException("Too many elements match the predicate");
})
.orElseThrow(() -> new IllegalStateException("No element matches the predicate"));
}
3. 使用Collectors.collectingAndThen
檢索唯一結果
Collectors.collectingAndThen
將函數應用於收集操作的結果List
。
因此,要定義 find 方法,我們需要獲取List
並且:
- 如果
List
有零個或兩個以上的元素,則返回null
- 如果
List
只有一個元素,則返回它
這是此操作的代碼:
private static <T> T findUniqueElement(List<T> elements) {
if (elements.size() == 1) {
return elements.get(0);
}
return null;
}
結果,find 方法顯示為:
public static <T> Optional<T> findUniqueElementMatchingPredicate_WithCollectingAndThen(Stream<T> elements, Predicate<T> predicate) {
return elements.filter(predicate)
.collect(Collectors.collectingAndThen(Collectors.toList(), list -> Optional.ofNullable(findUniqueElement(list))));
}
為了使我們的私有方法適用於 get 情況,如果檢索到的元素的數量不完全是 1,我們需要 throw。讓我們準確區分沒有結果和結果太多的情況,就像我們所做的那樣減少:
private static <T> T getUniqueElement(List<T> elements) {
if (elements.size() > 1) {
throw new IllegalStateException("Too many elements match the predicate");
} else if (elements.size() == 0) {
throw new IllegalStateException("No element matches the predicate");
}
return elements.get(0);
}
最後,鑑於我們將類命名為FilterUtils
,我們可以編寫 get 方法:
public static <T> T getUniqueElementMatchingPredicate_WithCollectingAndThen(Stream<T> elements, Predicate<T> predicate) {
return elements.filter(predicate)
.collect(Collectors.collectingAndThen(Collectors.toList(), FilterUtils::getUniqueElement));
}
4. 性能基準
讓我們使用 JMH 在不同方法之間進行快速的性能比較。
首先,讓我們將我們的方法應用於
- 包含從 1 到 100 萬的所有
Integers
的Stream
- 驗證元素是否等於 751879 的
Predicate
在這種情況下,將針對Stream
的一個唯一元素驗證Predicate
。讓我們看一下Benchmark
的定義:
@State(Scope.Benchmark)
public static class MyState {
final Stream<Integer> getIntegers() {
return IntStream.range(1, 1000000).boxed();
}
final Predicate<Integer> PREDICATE = i -> i == 751879;
}
@Benchmark
public void evaluateFindUniqueElementMatchingPredicate_WithReduction(Blackhole blackhole, MyState state) {
blackhole.consume(FilterUtils.findUniqueElementMatchingPredicate_WithReduction(state.INTEGERS.stream(), state.PREDICATE));
}
@Benchmark
public void evaluateFindUniqueElementMatchingPredicate_WithCollectingAndThen(Blackhole blackhole, MyState state) {
blackhole.consume(FilterUtils.findUniqueElementMatchingPredicate_WithCollectingAndThen(state.INTEGERS.stream(), state.PREDICATE));
}
@Benchmark
public void evaluateGetUniqueElementMatchingPredicate_WithReduction(Blackhole blackhole, MyState state) {
try {
FilterUtils.getUniqueElementMatchingPredicate_WithReduction(state.INTEGERS.stream(), state.PREDICATE);
} catch (IllegalStateException exception) {
blackhole.consume(exception);
}
}
@Benchmark
public void evaluateGetUniqueElementMatchingPredicate_WithCollectingAndThen(Blackhole blackhole, MyState state) {
try {
FilterUtils.getUniqueElementMatchingPredicate_WithCollectingAndThen(state.INTEGERS.stream(), state.PREDICATE);
} catch (IllegalStateException exception) {
blackhole.consume(exception);
}
}
讓我們運行它。我們正在測量每秒的操作數。越高越好:
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 140.581 ± 28.793 ops/s
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithReduction thrpt 25 100.171 ± 36.796 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 145.568 ± 5.333 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithReduction thrpt 25 144.616 ± 12.917 ops/s
正如我們所見,在這種情況下,不同方法的表現非常相似。
讓我們更改Predicate
以檢查Stream
的元素是否等於 0。對於List
的所有元素,此條件均為 false。我們現在可以再次運行基準測試:
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 165.751 ± 19.816 ops/s
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithReduction thrpt 25 174.667 ± 20.909 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 188.293 ± 18.348 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithReduction thrpt 25 196.689 ± 4.155 ops/s
再次,性能圖表非常平衡。
最後,讓我們看看如果我們使用Predicate
對大於 751879 的值返回 true 會發生什麼: List
中有大量元素與這個Predicate
匹配。這導致以下基準:
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 70.879 ± 6.205 ops/s
BenchmarkRunner.evaluateFindUniqueElementMatchingPredicate_WithReduction thrpt 25 210.142 ± 23.680 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithCollectingAndThen thrpt 25 83.927 ± 1.812 ops/s
BenchmarkRunner.evaluateGetUniqueElementMatchingPredicate_WithReduction thrpt 25 252.881 ± 2.710 ops/s
正如我們所看到的,減少的變體更有效。此外,直接在過濾後的Stream
上使用reduce
會大放異彩,因為在找到兩個匹配值後直接拋出Exception
。
簡而言之,如果性能是一個問題:
- 應優先使用減法
- 如果我們期望找到很多潛在的匹配值,那麼減少
Stream
的 get 方法要快得多
5. 結論
在本教程中,我們看到了過濾Stream
後檢索唯一結果的不同方法,然後比較了它們的效率。
與往常一樣,代碼可在 GitHub 上獲得。