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

时间:2017-08-28 14:21:28

标签: java java-8 filtering 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);

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

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

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

[编辑]答案比较

这里发布的答案有很多种,其中大多数都是有效的。为了回馈社区,我整理了一个small testing harness来比较这些算法的性能。

这种比较并没有我想要的那么广泛 - 已经有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.

由于其良好的性能和“黑盒”功能(状态管理代码在外部类中,并且贡献者可以专注于它),因此我认为这是我们在项目中实现的那个问题。商业逻辑)。

请注意,接受的答案可能不是最适合您的:查看其他答案,或查看my testing project以便亲自查看。

10 个答案:

答案 0 :(得分: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());

答案 1 :(得分:7)

据我所知,这已经写了 custom ,所以我在这里尝试了一个自定义收藏家:

Configure<T>

使用方法是:

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);
}

答案 2 :(得分:5)

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

class Fruit {
    String name;
    Long size;
}

以下内容可行:

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());
};

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

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

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

答案 3 :(得分:3)

关键行动是按照最小梨的方式按类型和大小排序 先走了。这样的事情:

// create a dummy pear; size value does not matter as comparing by ref
final Pear dummy = new Pear(-1);
basket
   // mix basket with the dummy pear
   .concat(basket, Stream.of(dummy))
      // sort by type so pears go first, then by size
      .sorted(Comparator
          .<Fruit>comparingInt(
              // arrange the dummy to always be the last 
              // among other pears but before other types 
              f -> (f == dummy ? 
                 0 : 
                 (Pear.class.equals(f.getClass()) ? -1 : 1))
          )
          .thenComparing(f -> f.size)
      )
      // skip the smallest pear
      .skip(1)
      // filter out the dummy
      .filter(f -> f != dummy)
      // sort again the rest by size
      .sorted(Comparator.comparingInt(f -> f.size))
      // take 20 at max
      .limit(20);

答案 4 :(得分:2)

您可以使用有状态谓词:

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);

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

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

如果您希望跳过多个元素,请将该参数添加到构造函数中,并在StatefulPredicate

中构建逻辑

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

修改

由于人们表示担心过滤器无状态,请从文档中

  

中间操作进一步分为无状态操作和有状态操作。无状态操作(例如过滤器和映射)在处理新元素时保留之前看到的元素的无状态 - 可以独立于其他元素上的操作处理每个元素。有状态操作(例如distinct和sorted)可以在处理新元素时包含先前看到的元素的状态。

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

此外,它可以使线程安全,以避免并发问题。

答案 5 :(得分:1)

不要尝试提前过滤。考虑

List<Fruit> output = basket.stream()
        .sorted(Comparator.comparing(Fruit::getSize))
        .limit(21)
        .collect(Collectors.toCollection(ArrayList::new));
int index = IntStream.range(0, output.size())
                     .filter(ix -> output.get(ix).isPear())
                     .findFirst().orElse(20);
if(index < output.size()) output.remove(index);

仅限21元素而不是20才能删除一个元素。通过使用Collectors.toCollection(ArrayList::new),您可以确保收到可变集合。

然后,有三种情况

  1. 该列表包含Pear。由于列表按水果大小排序,因此第一个Pear也将是最小的Pear,这是必须删除的… .findFirst()。随后的Pear将评估元素的索引。

  2. 该列表不包含21,但其大小为20。在这种情况下,我们必须删除最后一个元素,即索引.orElse(20),以获得所需的结果大小。这是由OptionalInt提供的,它会将空20映射到Pear

  3. 该列表可能不包含任何21且小于remove,因为源列表已经较小。在这种情况下,我们不会移除任何元素,通过在if(index < output.size())前加21操作进行检查。

  4. 正如我们之前已经知道的那样,这整个后处理可以被认为与性能无关,它将被应用于一个非常小的列表,在这个例子中最多只有basket个元素。这与源- name: My Great Playbook hosts: all gather_facts: False accelerate: False strategy: free vars: dbname: "@DBNAME@" repldbname: "connect to mydb" tasks: - block: - name: finding fl find: paths: "/home/username1/temp" patterns: "*.sql" file_type: "file" register: repos - name: some thing debug: msg="{{ item }}" with_items: "{{ repos.files }}" - name: replacing string replace: path: "{{ item }}" #path: "/home/username1/temp/1.sql" regexp: ({{ dbname }}) replace: '{{ repldbname }}' backup: no unsafe_writes: yes with_items: "{{ repos.files }}" 列表的大小无关。

答案 6 :(得分:0)

[更新],在阅读更新后的OP后,我对需求有了更好的理解:以下是StreamEx的更新代码:

Optional<Integer> smallestPear = StreamEx.of(basket).filter(Fruit::isPear)
                                         .mapToInt(Fruit::getSize).min();

StreamEx.of(basket)
        .chain(s -> smallestPear.map(v -> s.remove(f -> f.isPear() && f.getSize() == v).orElse(s))
        .sortedBy(Fruit::getSize).limit(20).toList();

[再次更新] 上述解决方案与Misha提供的解决方案非常相似。如果你不想经过两次流,如果篮子中的(水果类型,大小)对是唯一的话,这是另一种有限谓词的解决方案:

// Save this method in your toolkit.
public class Fn {
    public static <T> Predicate<T> limited(final Predicate<T> predicate, final int limit) {
        Objects.requireNonNull(predicate);    
        return new Predicate<T>() {
            private final AtomicInteger counter = new AtomicInteger(limit);
            @Override
            public boolean test(T t) {
                return predicate.test(t) && counter.decrementAndGet() >= 0;
            }
        };
    }
}

StreamEx.of(basket).sortedBy(Fruit::getSize)
        .remove(f -> Fn.limited(Fruit::isPear, 1))
        .limit(20).toList();

答案 7 :(得分:0)

我认为 ID Date v3 v4 1 2015.01.01 a 5 1 2015.02.01 b 5 1 2015.03.01 f 1 1 2015.04.01 z 5 1 2015.05.01 a 2 1 2015.06.01 a 2 2 2013.03.01 a 6 2 2013.04.01 a 2 2 2013.05.01 g 13 2 2013.06.01 a 2 2 2013.07.01 e 8 2 2013.08.01 h 9 2 2013.09.01 h 9 是您操作的原子操作符。因此,最简单的方法是编写自己的Predicate来包装原始的Predicate。假设名为Predicate的换行符,那么您的代码可以简化为如下所示:

once
output = basket.stream().sorted(comparing(Fruit::getSize))
                        .filter(once(Fruit::isPear))
                        .limit(20).collect(fruitCollector);

如果您想支持并发,则可以改为使用static <T> Predicate<T> once(Predicate<T> predicate){ boolean[] seen = {true}; return it -> !seen[0] || (seen[0]=predicate.test(it)); } ,例如:

AtomicInteger

答案 8 :(得分:0)

我遇到了同样的问题,但是我自己使用了Map和忽略列表来解决了。这是供您参考的示例。希望能有所帮助。

@Test
public void testGetStckTraceElements() {
    StackTraceElement[] stElements = Thread.currentThread().getStackTrace();

    // define a list for filter out
    List<String> ignoreClasses = Arrays.asList(
            Thread.class.getName(),
            this.getClass().getName()
    );

    // Map is using for check found before or not
    Map<String,Boolean> findFrist = new HashMap<String,Boolean>();
    Arrays.asList(stElements).stream()
        .filter(s -> {
            Platform.print("check: {}", s.getClassName());
            if (Optional.ofNullable(findFrist.get(s.getClassName())).orElse(false)) {
                return true;
            }
            findFrist.put(s.getClassName(), true);
            for (String className:ignoreClasses) {
                if (s.getClassName().equals(className)) return false;
            }

            return true;

        })
        .forEach(s->{
            Platform.print("Result: {} {} {} {}", s.getClassName(), s.getMethodName(), s.getFileName(), s.getLineNumber());
    });

}

答案 9 :(得分:-1)

这样的事情可能有用(但是如你所提到的那样分成2个篮子)

    Function<Fruit, Boolean> isPear = f -> f.getType().equals("Pear");
    Comparator<Fruit> fruitSize = Comparator.comparing(Fruit::getSize);
    Map<Boolean, List<Fruit>> pearsAndOthers = basket.sorted(fruitSize).limit(21).collect(Collectors.groupingBy(isPear));

    List<Fruit> pears = pearsAndOthers.get(true);
    List<Fruit> others = pearsAndOthers.get(false);

    Stream<Fruit> result;
    if (pears.size() == 0) {
        result = others.stream().limit(20);
    } else if (pears.size() == 1) {
        result = others.stream();
    } else {
        // You can probably merge in a nicer fashion since they should be sorted
        result = Stream.concat(pears.stream().skip(1), others.stream()).sorted(fruitSize);
    }