Подтвердить что ты не робот

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

Я застрял на краевом футляре в манипуляциях с потоками 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);

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

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

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


[Изменить] Сравнение ответов

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Изменить

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

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

Этот предикат не сохраняет информацию о ранее увиденных элементах. Он сохраняет информацию о предыдущих результатах.

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

Ответ 2

Считаете ли вы простой подход? Найдите самую маленькую грушу, отфильтруйте ее (если она существует) и соберите 20 самых маленьких:

Optional<Fruit> smallestPear = basket.stream()
        .filter(Fruit::isPear)  // or whatever it takes to test if it 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());

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

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

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), вы обеспечиваете получение изменчивой коллекции.

Затем есть три сценария

  • Список содержит Pear. Поскольку список сортируется по размерам фруктов, первый Pear также будет наименьшим Pear, который должен быть удален. Последующий … .findFirst() будет оценивать индекс элемента.

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

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

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

Ответ 7

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

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

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

Ответ 8

Я думаю, что 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, например:

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

Ответ 9

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