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