过滤Java 8 Stream
,通常使用findFirst()
或findAny()
来获取在过滤器中幸存的元素。 但这可能并不能真正实现您的意思,并且可能会出现一些细微的错误。
那么
从我们的Javadoc( 此处和此处 )可以看出,这两个方法都从流中返回任意元素-除非流具有遇到顺序 ,在这种情况下, findFirst()
返回第一个元素。 简单。
一个简单的示例如下所示:
public Optional<Customer> findCustomer(String customerId) {
return customers.stream()
.filter(customer -> customer.getId().equals(customerId))
.findFirst();
}
当然,这只是旧的for-each-loop的漂亮版本:
public Optional<Customer> findCustomer(String customerId) {
for (Customer customer : customers)
if (customer.getId().equals(customerId))
return Optional.of(customer);
return Optional.empty();
}
但是,这两种变体都包含相同的潜在错误:它们是基于隐含的假设而建立的,即只有一个具有任何给定ID的客户。
现在,这可能是一个非常合理的假设。 也许这是一个已知的不变式,由系统的专用部分保护,并由其他人员依赖。 在那种情况下,这是完全可以的。
通常,代码依赖于唯一的匹配元素,但是没有做任何断言。
但是,在许多情况下,我并不是在野外看到的。 也许客户只是从外部来源加载的,这些来源无法保证其ID的唯一性。 也许现有的错误允许两本书具有相同的ISBN。 也许搜索词允许出乎意料的许多意外匹配(有人说过正则表达式吗?)。
通常,代码的正确性取决于以下假设:存在与条件匹配的唯一元素,但它不执行或声明此条件。
更糟糕的是,不当行为完全是由数据驱动的,可能会在测试期间将其隐藏。 除非我们牢记这种情况,否则我们可能会完全忽略它,直到它在生产中显现出来为止。
更糟糕的是,它默默地失败了! 如果只有一个这样的元素的假设被证明是错误的,我们将不会直接注意到这一点。 取而代之的是,系统会在观察到影响并查明原因之前巧妙地表现出一段时间。
因此,当然, findFirst()
和findAny()
本身并没有错。 但是,使用它们很容易导致建模域逻辑中的错误。
快速失败
因此,让我们解决这个问题! 假设我们非常确定最多有一个匹配元素,如果没有,我们希望代码快速失败 。 通过循环,我们必须管理一些难看的状态,它看起来如下:
public Optional<Customer> findOnlyCustomer(String customerId) {
boolean foundCustomer = false;
Customer resultCustomer = null;
for (Customer customer : customers)
if (customer.getId().equals(customerId))
if (!foundCustomer) {
foundCustomer = true;
resultCustomer = customer;
} else {
throw new DuplicateCustomerException();
}
return foundCustomer
? Optional.of(resultCustomer)
: Optional.empty();
}
现在,流为我们提供了一种更好的方法。 我们可以使用经常被忽略的reduce, 文档中对此说 :
使用关联累加函数对此流的元素进行归约 ,并返回一个Optional描述归约值(如果有)。 这等效于:
流减少
boolean foundAny = false; T result = null; for (T element : this stream) { if (!foundAny) { foundAny = true; result = element; } else result = accumulator.apply(result, element); } return foundAny ? Optional.of(result) : Optional.empty();
但不限于顺序执行。
看起来不像上面的循环吗?! 疯狂的巧合...
因此,我们需要的是一个累加器,该累加器会在调用后立即抛出所需的异常:
public Optional<Customer> findOnlyCustomerWithId_manualException(String customerId) {
return customers.stream()
.filter(customer -> customer.getId().equals(customerId))
.reduce((element, otherElement) -> {
throw new DuplicateCustomerException();
});
}
这看起来有些奇怪,但确实可以满足我们的要求。 为了使其更具可读性,我们应该将其放入Stream实用工具类中,并为其命名一个漂亮的名称:
public static <T> BinaryOperator<T> toOnlyElement() {
return toOnlyElementThrowing(IllegalArgumentException::new);
}
public static <T, E extends RuntimeException> BinaryOperator<T>
toOnlyElementThrowing(Supplier<E> exception) {
return (element, otherElement) -> {
throw exception.get();
};
}
现在我们可以这样称呼它:
// if a generic exception is fine
public Optional<Customer> findOnlyCustomer(String customerId) {
return customers.stream()
.filter(customer -> customer.getId().equals(customerId))
.reduce(toOnlyElement());
}
// if we want a specific exception
public Optional<Customer> findOnlyCustomer(String customerId) {
return customers.stream()
.filter(customer -> customer.getId().equals(customerId))
.reduce(toOnlyElementThrowing(DuplicateCustomerException::new));
}
目的显示代码如何?
这将实现整个流。
应该注意的是,与findFirst()
和findAny()
,这当然不是短路操作 ,它将实现整个流。 也就是说,如果确实只有一个元素。 当然,一旦遇到第二个元素,处理就会停止。
反射
我们已经看到findFirst()
和findAny()
如何不足以表示流中最多剩余一个元素的假设。 如果我们要表达该假设,并确保在违反代码时代码快速失败,则需要reduce(toOnlyElement())
。
首先感谢Boris Terzic使我意识到这种意图不匹配。
翻译自: https://www.javacodegeeks.com/2016/02/beware-findfirst-findany.html