1. 概述
在本文中,我们将使用Collectors中的两种方法来检索与给定元素流中的特定谓词匹配的唯一元素。
对于这两种方法,我们将根据以下标准定义两种方法:
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将函数应用于收集操作的结果列表。
因此,要定义find方法,我们需要获取List并且:
- 如果List包含0个或2个以上的元素,则返回null
- 如果List只有1个元素,则返回它
这是此操作的代码:
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,我们需要抛出。让我们准确区分没有结果和结果太多的情况,就像我们对归约所做的那样:
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万的所有整数的Stream
- 验证元素是否等于751879的Predicate
在这种情况下,Predicate将针对Stream的一个唯一元素进行验证。让我们看一下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
同样,性能图表非常平衡。
最后,让我们看看如果我们使用对大于751879的值返回true的Predicate会发生什么: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上获得。