Как отфильтровать только первый элемент, не соответствующий предикату в последовательном потоке 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 ответов:
Можно использовать предикат с сохранением состояния:
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()); };
И:
Мой компаратор поместит самый маленький персик в 21-ю позицию, сохранит порядок другихoutput = basket.stream() .sorted(Comparator.comparing(Fruit::getSize)) .limit(21) .sorted(fruitComparator) .limit(20) .sorted(Comparator.comparing(Fruit::getSize)) .collect(fruitCollector);
Fruit
s естественным, так что в случае, если нет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)
, вы гарантируете получение изменяемой коллекции.Тогда есть три сценария
Всю эту постобработку можно считать несущественной для производительности, так как мы уже заранее знаем, что она будет применена к очень небольшому списку, содержащему не более
Список содержит a
Pear
. Поскольку список отсортирован по размерам плодов, первыйPear
также будет самым маленькимPear
, который должен быть удален. Последующие… .findFirst()
будут оценивать по индексу элемент.Список не содержит
Pear
, но имеет размер21
. В этом случае мы должны удалить последний элемент, т. е. в индексе20
, чтобы получить желаемый размер результата. Это обеспечивается.orElse(20)
, который сопоставит пустоеOptionalInt
с20
.Список может не содержать никаких
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); }