Java 流:首先查找多个过滤谓词

Java Streams: Find first for multiple filter predicates

我有一个 Collection<Product> 和一个 Collection<Predicate<Product>>。谓词是布尔标志的简单组合。例如:

             | isNew | isSoldOut
--------------------------------
predicate 1: | false | false
predicate 2: | false | true
predicate 3: | true  | false
predicate 4: | true  | true

我想找到“每一种”,虽然我想找到产品集合中每个谓词的第一个匹配项。

目前我的代码是这样的:

List<Product> products = getProducts();
List<Predicate<Product>> predicates = getPredicates();

List<Product> result = predicates.stream()
  .flatMap(predicate -> products.stream().filter(predicate).findFirst().stream())
  .collect(Collectors.toList());

但这当然会多次迭代产品集合,这是不希望的,因为在我的例子中,我有 100000 个产品和 64 个谓词,这需要很长时间。

在我的特殊情况下,谓词是互斥的:如果谓词 returns 为真,则可以跳过该特定产品的所有其他谓词。而且因为我使用 findFirst 这个谓词然后可以跳过所有其他产品。

我想知道是否可以迭代 Product Collection,并针对所有 Predicates 对每个 Product 只检查一次。

如果我没理解错的话,你正在寻找类似这样的东西:

List<Product> results = products.stream()
                        .filter(prod -> predicates.stream()
                                        .anyMatch(pred -> pred.test(prod)))
                        .collect(Collectors.toList());

反过来做怎么样?流式传输产品,并对其应用谓词。

List<Predicate<Product>> predicates = getPredicates();
List<Product> products = getProducts();
List<Product> filtered = products.stream().filter(product -> {
    Iterator<Predicate<Product>> iterator = predicates.iterator();
    while (iterator.hasNext()) {
        Predicate<Product> currentPredicate = iterator.next();
        if (currentPredicate.test(product)) {
             iterator.remove();
             return true;
        }
    }
    return false;
}).collect(Collectors.toList());

缺点是您必须小心将哪个集合用于谓词,Iterator.remove 并不总是受支持。

编辑: 看来我没有仔细阅读。我认为最好用循环获得每一个。

List<Product> products = getProducts();
List<Predicate<Product>> predicates = getPredicates();
List<Product> matchingProducts = new ArrayList<>(predicates.size());
for (Product product : products) {
    if (predicates.isEmpty()) {
        break;
    }
    for (int predicateIndex = 0; predicateIndex < predicates.size(); predicateIndex++) {
        Predicate<Product> predicate = predicates.get(predicateIndex);
        if (predicate.test(product)) {
            matchingProducts.add(product);
            predicates.remove(predicateIndex);
            break;
        }
    }
}

实际上是通过流实现的,takeWhile,你是对的,本杰明。

List<Predicate<Product>> predicates = getPredicates();
List<Product> products = getProducts();
List<Product> matches = products.stream()
        .takeWhile(product -> !predicates.isEmpty())
        .filter(product -> {
            Iterator<Predicate<Product>> iterator = predicates.iterator();
            while (iterator.hasNext()) {
                if (iterator.next().test(product)) {
                    iterator.remove();
                    return true;
                }
            }
            return false;
        })
        .collect(Collectors.toList());

只需确保 takeWhile filter 之前,否则将跳过最后一个匹配元素。

您当前的解决方案将多次遍历集合,但由于 findFirst 是一个 short-circuiting 运算符,它会在找到匹配项后立即停止。你有bench-marked它来确保它不够好吗?

另一种方法是使用 状态过滤器(参见 this post 的最佳答案):

public static Predicate<Product> matchAndDiscard(final List<Predicate<Product>> predicates) {
  final Set<Predicate<Product>> remaining = new HashSet<>(predicates);
  return product -> {
    final var match = remaining.stream().filter(pred -> pred.test(product)).findFirst();
    match.ifPresent(remaining::remove);
    return match.isPresent();
  };
}

很像@Chaosfire 的方法,但状态包含在过滤器函数中。如果您认为所有谓词都会被至少一种产品匹配,您还可以通过将流限制为谓词的数量来节省一些时间,如下所示:

final var predicates = getPredicates()
final var result = getProducts().stream()
    .filter(matchAndDiscard(predicates))
    .limit(predicates.size())
    .toList()

在您当前的解决方案中,您将“水平”遍历产品:

       --> products
pred1: ffffffffffffft
pred2: fffft
pred3: ffffffffffffffft
pred4: ft
etc.

替代方案将进行“垂直”遍历:

           products
pred1: | ffffffffffffft
pred2: | fffft
pred3: v ffff fffffffffft
pred4:   ft

所以一个比另一个快得多并不明显,这取决于特定的配置。