【问题标题】:Return first result matching predicate in a Java stream or all non-matching results返回 Java 流中的第一个结果匹配谓词或所有不匹配的结果
【发布时间】:2018-08-06 11:11:16
【问题描述】:

我有一个Validator 接口,它提供了一个isValid(Thing) 方法,返回一个ValidationResult,其中包含一个boolean 和一个原因消息。

我想创建此接口的ValidatorAggregator 实现,该实现跨多个Validators 执行OR(如果任何Validator 返回正结果,则结果为正)。如果任何验证器成功,我想短路并返回其结果。如果没有验证器成功,我想返回所有失败消息。

我可以使用流和findFirst().orElse(...) 简洁地做到这一点,但使用这种模式,如果findFirst 返回空,我会丢失所有中间结果:

public ValidationResult isValid(final Thing thing) {
    return validators.stream()
      .map(v -> validator.isValid(thing))
      .filter(ValidationResult::isValid)
      .findFirst()
      .orElseGet(() -> new ValidationResult(false, "All validators failed'));
}

有什么方法可以使用流捕获失败的结果,或者比下面的更简洁?

public ValidationResult isValid(final Thing thing) {
    final Set<ValidationResult> failedResults = new HashSet<>();
    for (Validator validator : validators) {
        final ValidationResult result = validator.isValid(thing);
        if (result.isValid()) {
            return result;
        }
        failedResults.add(result);
    }
    return new ValidationResult(false, "No successful validator: " + failedResults); 
    // (assume failedResults stringifies nicely)
}

编辑:基于 cmets,我同意我正在尝试做的是过早优化(特别是因为这些验证器非常轻量级)。我可能会采用类似于 Holger 的计算所有验证并划分成成功/不成功结果的解决方案。

这被标记为 Can you split a stream into two streams? 的欺骗,而 partitioningBy 的答案是,但我认为这个问题是在问,而讨论的答案是另一个问题。 p>

【问题讨论】:

  • 非流实现将很难被击败。
  • 您的流解决方案,即使没有保存中间结果的功能,在我看来也不比非流解决方案更简洁。
  • 正如其他人提到的,很难获得短路行为。另一方面,考虑这是否不是过早优化的情况。
  • 你可以为此旋转一个自定义收集器,但是它不会像你的 for 循环那样既短也不短路,所以坚持下去
  • 任何变体都是一种权衡,可能会执行不必​​要的操作。您应该使用最适合可能性最高的情况的变体。

标签: java java-8 java-stream aggregate predicate


【解决方案1】:

没有完美的解决方案可以以相同的效率处理所有案件。即使您的循环变体满足了短路和仅处理一次验证器的标准,也存在创建和填充集合的缺点,如果只有一次验证成功,这可能会变得不必要。

选择取决于与操作相关的实际成本以及至少进行一次成功验证的可能性。如果以最佳性能处理常见情况,则可能会超过解决方案对处理不常见情况的惩罚。

所以

// you may use this if the likelihood of a success is high; assumes
// reasonable costs for the validation and consists (repeatable) results
public ValidationResult isValid(final Thing thing) {
    return validators.stream()
      .map(v -> v.isValid(thing))
      .filter(ValidationResult::isValid)
      .findFirst()
      .orElseGet(() -> new ValidationResult(false, "All validators failed"
        + validators.stream().map(v -> v.isValid(thing)).collect(Collectors.toSet())));
}
// you may use this if the likelihood of a success is
// very low and/or you intent to utilize parallel processing
public ValidationResult isValid(final Thing thing) {
    Map<Boolean,Set<ValidationResult>> results = validators.stream()
        .map(v -> v.isValid(thing))
        .collect(Collectors.partitioningBy(ValidationResult::isValid, Collectors.toSet()));
    return results.get(true).stream().findAny()
        .orElseGet(() -> new ValidationResult(false,
                             "No successful validator: "+results.get(false)));
}
// if chances of successful validation are mixed or unpredictable
// or validation is so expensive that everything else doesn't matter
// stay with the loop
public ValidationResult isValid(final Thing thing) {
    final Set<ValidationResult> failedResults = new HashSet<>();
    for (Validator validator : validators) {
        final ValidationResult result = validator.isValid(thing);
        if (result.isValid()) {
            return result;
        }
        failedResults.add(result);
    }
    return new ValidationResult(false, "No successful validator: " + failedResults);
}

考虑对列表进行排序,使成功机会更高的验证者排在最前面……

【讨论】:

  • 感谢您提供不同的选项。我认为我对您的回答 + Apokralipsa 的关键点是一致的,即短路可能是过早的优化。因此,您提供的第二个选项(或其中的一些变体,有/没有流)可能是最好和最简单的一个:只需评估它们。
  • 我会将其标记为已接受的答案,因为虽然它没有直接解决我提出的问题,但它确实提供了几种正确的方法来完成我最终想要实现的目标。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-01
  • 2021-08-06
  • 1970-01-01
  • 1970-01-01
  • 2013-04-18
相关资源
最近更新 更多