如何只筛选出与Java顺序流中的谓词不匹配的第一个元素?

Sil*_*ier 17 java filtering java-8 java-stream

我在Java流操作中遇到了边缘情况......

我想编码以下行为:"从任意一篮子水果中收集20个最小的,除了最小的梨,因为我们不希望这样."

额外的奖励:来的篮子可能根本没有任何梨.

例子 :

  • 从[Pear 5,Apple 1,Apple 2,Apple 10,Pear 3,Pear 7]开始,我们需要[Apple 1,Apple 2,Pear 5,Pear 7,Apple 10].
  • 从[Apple 4,Apple 7,Pear 8,Pear 2,Pear 3]开始,我们想要[Pear 3,Apple 4,Apple 7,Pear 8].

到目前为止,我正迈出这一步:

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

这似乎是状态 lambda过滤器的情况,我不知道该怎么做.

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

最糟糕的情况我可以将篮子分成两个,梨和非梨,对梨进行分类,如果有的话,适当地对它们进行子目录.这似乎非常低效和丑陋.有没有更好的办法?


[编辑]答案比较

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

这种比较并没有我想要的那么广泛 - 已经有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'答案是因为它是我们在项目中实现的那个).

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

Mis*_*sha 8

你考虑过一种直截了当的方法吗?找到最小的梨,将其过滤掉(如果存在)并收集最小的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());
Run Code Online (Sandbox Code Playgroud)

  • 您是否确实存在性能问题(以及衡量指标的指标)?您可能会发现性能成本将由排序控制,并进行简单的扫描以找到最小的梨没什么区别.此外,传统的智慧是编码清晰,只有在你有理由的情况下才能优化性能.我发现我认为最容易确信的方法.我当然觉得它更适合有状态谓词或定制收藏家.但清晰度在旁观者眼中,所以你决定. (3认同)

Eug*_*ene 7

据我所知,这已经定制了全部,所以我在这里尝试了一个自定义收集器:

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);
}
Run Code Online (Sandbox Code Playgroud)

用法是:

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

  • 我喜欢Collector解决方案!是否有助于性能还将treeSets的大小限制为"size",即如果集合超过`accumulate`或`combine`中的`size`,则删除最大的元素?它在添加更多元素时保持较小的比较次数,并有助于在输入变大时保持所保持的引用数量.但是,这些收益可能会被大小检查和删除呼叫所抵消. (2认同)
  • @MalteHartwig我最初考虑过这个问题 - 但不能决定这样做,对于大型数据集你肯定是对的,但对于小而中等 - 我说不出来.这将需要大量的测量...可能我会尝试两个并测试.感谢您的评论. (2认同)
  • 我喜欢从篮子到树上的水果流的想法;) (2认同)

xen*_*ros 5

为了便于实现,我附上了一个示例:

class Fruit {
    String name;
    Long size;
}
Run Code Online (Sandbox Code Playgroud)

以下将有效:

Comparator<Fruit> fruitComparator = (o1, o2) -> {

    if (o1.getName().equals("Peach") && o2.getName().equals("Peach")) {
        return o2.getSize().compareTo(o1.getSize()); //reverse order of Peaches
    }

    if (o1.getName().equals("Peach")) {
        return 1;
    }
    if (o2.getName().equals("Peach")) {
        return -1;
    }
    return o1.getSize().compareTo(o2.getSize());
};
Run Code Online (Sandbox Code Playgroud)

和:

output = basket.stream()
    .sorted(Comparator.comparing(Fruit::getSize))
    .limit(21)
    .sorted(fruitComparator)
    .limit(20)
    .sorted(Comparator.comparing(Fruit::getSize))
    .collect(fruitCollector);
Run Code Online (Sandbox Code Playgroud)

我的比较器将最小的Peach放到第21位,将保持其他Fruits 的顺序自然,所以如果没有Peach,它将返回第21个最大的元素.然后我按正常顺序对其余部分进行排序.

这会奏效.这是一个黑客,在某些情况下可能是一个糟糕的选择.我想指出,排序20个元素应该不是问题.

  • 我会说,在商业项目中,这些事件的出现频率要高得多.虽然我已经参与了一个关于蔬菜和水果的项目:D (2认同)
  • 那么如果有不到20个水果怎么办? (2认同)

ped*_*mss 3

您可以使用有状态谓词:

class StatefulPredicate<T> implements Predicate<T> {

    private boolean alreadyFiltered;
    private Predicate<T> pred;

    public StatefulPredicate(Predicate<T> pred) {
        this.pred = pred;
        this.alreadyFiltered = false;
    }

    @Override
    public boolean test(T t) {
        if(alreadyFiltered) {
            return true;
        }

        boolean result = pred.test(t);
        alreadyFiltered = !result;
        return result;
    }
}

    Stream.of(1, -1, 3, -4, -5, 6)
        .filter(new StatefulPredicate<>(i -> i > 0))
        .forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

印刷:1, 3, -4, -5, 6

如果并发是一个问题,您可以使用原子布尔值。

如果您希望跳过 1 个以上元素,请将该参数添加到您的构造函数中并在其中构建您的逻辑StatefulPredicate

该谓词过滤第一个负元素,然后让所有其他元素通过,无论如何。在你的情况下你应该测试instanceof Pear

编辑

由于人们对过滤器无状态表示担忧,从文档中可以看出:

中间操作又分为无状态操作和有状态操作。无状态操作(例如过滤器和映射)在处理新元素时不保留先前看到的元素的状态- 每个元素都可以独立于其他元素上的操作进行处理。有状态操作(例如不同和排序)可以在处理新元素时合并先前看到的元素的状态。

该谓词不保留有关先前看到的元素的信息。它保留有关先前结果的信息。

还可以使其线程安全以避免并发问题。

  • “您可以使用有状态谓词”不,您不能。[Stream.filter的文档](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#filter-java.util.function.Predicate-)非常明确:谓词必须是“非干扰、无状态谓词”。 (2认同)
  • @pedromss 针对有状态谓词的建议绝对适用于您的解决方案。它受文档中描述的所有警告的约束。说它只保留有关结果的信息并没有帮助:这些结果取决于元素,而过滤器的状态反映了已看到哪些元素。 (2认同)