Как отфильтровать только первый элемент, не соответствующий предикату в последовательном потоке Java?


Я застрял на крайнем случае в потоковых манипуляциях java...

Я хочу закодировать следующее поведение: "из произвольной корзины фруктов соберите 20 самых маленьких, кроме самой маленькой груши, потому что мы этого не хотим."

Дополнительный бонус: в корзинах, которые придут, возможно, вообще не будет груши.

Примеры:

  • из [Груша 5, Яблоко 1, яблоко 2, Яблоко 10, Груша 3, груша 7], мы хотим [Яблоко 1, яблоко 2, Груша 5, груша 7, Яблоко 10].
  • Из [Apple 4, Apple 7, 8 груш, 2 груши, Груша 3] мы хотим [3 груши, 4 яблока, Яблоко-7 груша 8].

Пока что я на этом этапе:

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

Это похоже на случай stateful Lambda-фильтра, и я не знаю, как это сделать.

Я не могу использовать локальное firstPear булево и установить его в true после фильтрации первой груши, так как все локальные переменные в лямбде должны быть конечными.

В худшем случае я могу разделить корзину на две части, груши и не груши, отсортировать груши и внести их в список. уместно, если таковые имеются. Это кажется очень неэффективным и уродливым. есть ли лучший способ?


[Edit] сравнение ответов

Ответы, опубликованные здесь, отличались большим разнообразием, и большинство из них были правильными. Чтобы вернуть сообщество, я собрал небольшой тестовый жгутдля сравнения производительности этих алгоритмов. Это сравнение было не таким обширным, как я хотел - прошло уже 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, так как он реализован в проекте, благодаря как его хорошей производительности, так и возможностям "черного ящика" (код управления состоянием находится во внешнем классе, и участники могут сосредоточиться на бизнес-логике).

Обратите внимание, что принятый ответ может не быть лучшее для вас: просмотрите другие или проверьте мой тестовый проект , чтобы убедиться в этом самостоятельно.

10 17

10 ответов:

Можно использовать предикат с сохранением состояния:

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

Если параллелизм является проблемой, вы можете использовать атомарный логический символ.

Если вы хотите пропустить более 1 элемента, добавьте этот параметр в конструктор и постройте свою логику внутри StatefulPredicate

Этот предикат фильтрует первый отрицательный элемент, а затем пропускает все остальные элементы, независимо от этого. В вашем случае вы должны проверить для instanceof Pear

Edit

Так как люди проявляли беспокойство по поводу того, что фильтр является без гражданства, из документации:

Промежуточные операции далее подразделяются на операции без состояния и операции с сохранением состояния. Операции без сохранения состояния, такие как filter и map, не сохраняют состояния из ранее виденного элемента при обработке нового элемента - каждый элемент может быть обработан независимо от операций над другими элементами. Операции с сохранением состояния, такие как distinct и sorted, могут включать состояние из ранее виденных элементов при обработке новых элементы.
Этот предикат не сохраняет информацию о ранее виденных элементах. Он сохраняет информацию о предыдущих результатах.

Также можно сделать потокобезопасным, чтобы избежать проблем параллелизма.

Рассматривали ли вы прямой подход? Найдите самую маленькую грушу, отфильтруйте ее (если она существует) и соберите 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());

Насколько я могу судить, это имеет обычай написано на нем, поэтому я попробовал пользовательский коллектор здесь:

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

Для упрощения реализации я привожу пример для:

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);
Мой компаратор поместит самый маленький персик в 21-ю позицию, сохранит порядок других Fruits естественным, так что в случае, если нет Peach, он вернет 21-й самый большой элемент. Затем я сортирую остальное в обычном порядке.

Это сработает. Это хак, и в некоторых обстоятельствах это может быть плохой выбор. Я хотел бы отметить, что сортировка 20 элементы не должны быть проблемой.

Ключевым действием является сортировка по типу и размеру таким образом, чтобы самая маленькая груша идти сначала. Что-то вроде этого:

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

Не пытайтесь фильтровать заранее. Рассмотрим

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. Список содержит a Pear. Поскольку список отсортирован по размерам плодов, первый Pear также будет самым маленьким Pear, который должен быть удален. Последующие … .findFirst() будут оценивать по индексу элемент.

  2. Список не содержит Pear, но имеет размер 21. В этом случае мы должны удалить последний элемент, т. е. в индексе 20, чтобы получить желаемый размер результата. Это обеспечивается .orElse(20), который сопоставит пустое OptionalInt с 20.

  3. Список может не содержать никаких Pear и быть меньше, чем 21, потому что исходный список уже был меньше. В этом случае мы не удаляем никакие элементы, проверенные предварением операции remove с if(index < output.size()).

Всю эту постобработку можно считать несущественной для производительности, так как мы уже заранее знаем, что она будет применена к очень небольшому списку, содержащему не более 21 элементов в этом примере. Это не зависит от размера исходного списка basket.

[Update], прочитав обновленный 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();

[обновить еще раз] Вышеприведенное решение очень похоже на решение, предложенное Мишей. если вы не хотите проходить через поток дважды, Вот еще одно решение с помощью ограниченного предиката, если пара (тип плода, размер) в корзине уникальна:

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

Я думаю, что 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));
}

Если вы хотите поддерживать concurrent, вы можете использовать AtomicInteger вместо этого, например:

static <T> Predicate<T> once(Predicate<T> predicate){
   AtomicInteger seen = new AtomicInteger(0);

   return it -> {
     //if seen==0 then test predicate, otherwise increment only 
     IntBinaryOperator accumulator = (x,y)-> x==0 && predicate.test(it) ? x : x+y;
     return seen.accumulateAndGet(1, accumulator) != 1; 
   };
}

У меня та же проблема, но я решил ее сам, используя карту и список игнорирования. Вот образец для вашего сведения. Надежда может помочь.

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

}

Что-то вроде этого может сработать (однако группы в 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);
    }