如何仅过滤掉Java顺序流中与谓词不匹配的第一个元素?

2022-09-02 21:06:05

我被困在Java流操作的边缘情况...

我想对以下行为进行编码:“从任意一篮子水果中,收集20个最小的,除了最小的梨,因为我们不想要那个。

额外的好处:即将到来的篮子可能根本没有梨。

例子:

  • 从[梨5,苹果1,苹果2,苹果10,梨3,梨7],我们想要[苹果1,苹果2,梨5,梨7,苹果10]。
  • 从[苹果4,苹果7,梨8,梨2,梨3],我们想要[梨3,苹果4,苹果7,梨8]。

到目前为止,我处于这一步:

output = basket.stream()
    .sorted(Comparator.comparing(Fruit::getSize))
    //.filter(???)
    .limit(20)
    .collect(fruitCollector);

这似乎是有状态lambda过滤器的一个案例,我不知道该怎么做。

我不能使用局部布尔值并将其设置为在过滤第一个梨之后,因为lambda中的所有局部变量都必须是最终的。firstPeartrue

最坏的情况是,我可以把篮子分成两半,梨和非梨,对梨进行排序,如果有的话,可以适当地将它们子列出。这似乎非常低效和丑陋。有没有更好的方法?


[编辑]答案比较

这里发布的答案有很多变化,其中大多数都是有效的。为了回馈社区,我整理了一个小的测试工具来比较这些算法的性能。

这种比较并不像我想要的那么广泛 - 已经3周了。它仅涵盖简单项目的顺序处理的用法。您可以随意尝试测试工具,并添加更多测试、更多基准测试或您自己的实现。

我的分析:

Algorithm                | Author   | Perf | Comments
--------------------------------------------------------------------------------
Indexed removal          | Holger   | Best | Best overall, somewhat obscure
Stateful predicate       | pedromss | Best | Do not use for parallel processing
Straightforward approach | Misha    | Best | Better when few elements match
Custom collector         | Eugene   | Good | Better when all or no element match
Comaprator hack w/ dummy | yegodm   | Good | -
Comparator hack          | xenteros | *    | Perf sensitive to output size, fails on edge cases.

我回答了 pedromss 的答案,因为它是我们在项目中实现的,因为它具有良好的性能和“黑匣子”功能(状态管理代码位于外部类中,贡献者可以专注于业务逻辑)。

请注意,接受的答案可能不适合您:查看其他答案,或检查我的测试项目以亲自查看。


答案 1

你有没有考虑过一种直截了当的方法?找到最小的梨,过滤掉它(如果存在)并收集20个最小的梨:

Optional<Fruit> smallestPear = basket.stream()
        .filter(Fruit::isPear)  // or whatever it takes to test if it's a pear
        .min(Fruit::getSize);

Stream<Fruit> withoutSmallestPear = smallestPear
        .map(p -> basket.stream().filter(f -> f != p))
        .orElseGet(basket::stream);

List<Fruit> result = withoutSmallestPear
        .sorted(comparing(Fruit::getSize))
        .limit(20)
        .collect(toList());

答案 2

据我所知,这已经习惯写满了它,所以我确实在这里尝试了一个自定义收集器:

private static <T> Collector<T, ?, List<T>> exceptCollector(Predicate<T> predicate, int size, Comparator<T> comparator) {

    class Acc {

        private TreeSet<T> matches = new TreeSet<>(comparator);

        private TreeSet<T> doesNot = new TreeSet<>(comparator);

        void accumulate(T t) {
            if (predicate.test(t)) {
                matches.add(t);
            } else {
                doesNot.add(t);
            }
        }

        Acc combine(Acc other) {

            matches.addAll(other.matches);
            doesNot.addAll(other.doesNot);

            return this;
        }

        List<T> finisher() {
            T smallest = matches.first();
            if (smallest != null) {
                matches.remove(smallest);
            }

            matches.addAll(doesNot);
            return matches.stream().limit(size).collect(Collectors.toList());
        }

    }
    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::finisher);
}

用法是:

List<Fruit> fruits = basket.getFruits()
            .stream()
            .collect(exceptCollector(Fruit::isPear, 20, Comparator.comparing(Fruit::getSize)));