Java 8 Stream API - выберите самый низкий ключ после группы
У меня есть поток объектов Foo.
class Foo {
private int variableCount;
public Foo(int vars) {
this.variableCount = vars;
}
public Integer getVariableCount() {
return variableCount;
}
}
Мне нужен список Foo
, у которого все самые низкие переменные.
Например
new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1)
Я хочу, чтобы поток возвращал последние 2 Foo
Я попытался сделать сбор с группировкой
.collect(Collectors.groupingBy((Foo foo) -> {
return foo.getVariableCount();
})
И это возвращает Map<Integer, List<Foo>>
и я не уверен, как преобразовать это в то, что я хочу.
заранее спасибо
Ответы
Ответ 1
Вот решение, которое:
- Только поток только один раз.
- Не создает карту или другую структуру, которая содержит все входные элементы (если только значения переменных не совпадают), сохраняя только те, которые в настоящее время являются минимальными.
- O (n) время, O (n). Вполне возможно, что все
Foo
имеют одинаковое количество переменных, и в этом случае это решение будет хранить все элементы, такие как другие решения. Но на практике, с различными, разнообразными значениями и более высокой мощностью, количество элементов в списке, вероятно, будет намного ниже.
отредактированный
Я улучшил свое решение в соответствии с предложениями в комментариях.
Я реализовал объект-аккумулятор, который для этого выполняет функции Collector
.
/**
* Accumulator object to hold the current min
* and the list of Foos that are the min.
*/
class Accumulator {
Integer min;
List<Foo> foos;
Accumulator() {
min = Integer.MAX_VALUE;
foos = new ArrayList<>();
}
void accumulate(Foo f) {
if (f.getVariableCount() != null) {
if (f.getVariableCount() < min) {
min = f.getVariableCount();
foos.clear();
foos.add(f);
} else if (f.getVariableCount() == min) {
foos.add(f);
}
}
}
Accumulator combine(Accumulator other) {
if (min < other.min) {
return this;
}
else if (min > other.min) {
return other;
}
else {
foos.addAll(other.foos);
return this;
}
}
List<Foo> getFoos() { return foos; }
}
Тогда все, что нам нужно сделать, это collect
, ссылаясь на методы аккумулятора для своих функций.
List<Foo> mins = foos.stream().collect(Collector.of(
Accumulator::new,
Accumulator::accumulate,
Accumulator::combine,
Accumulator::getFoos
)
);
Тестирование с помощью
List<Foo> foos = Arrays.asList(new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1), new Foo(4));
Выход (с подходящей toString
определенной на Foo
):
[Foo{1}, Foo{1}]
Ответ 2
Вы можете использовать отсортированную карту для группировки, а затем просто получить первую запись. Что-то вроде строк:
Collectors.groupingBy(
Foo::getVariableCount,
TreeMap::new,
Collectors.toList())
.firstEntry()
.getValue()
Ответ 3
ЕСЛИ вы нормально потоки (итерации) дважды:
private static List<Foo> mins(List<Foo> foos) {
return foos.stream()
.map(Foo::getVariableCount)
.min(Comparator.naturalOrder())
.map(x -> foos.stream()
.filter(y -> y.getVariableCount() == x)
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
Ответ 4
Чтобы избежать создания карты, вы можете использовать два потока:
- первый находит минимальное значение.
- второй фильтрует элементы с этим значением.
Это могло бы дать:
List<Foo> foos = ...;
int min = foos.stream()
.mapToInt(Foo::getVariableCount)
.min()
.orElseThrow(RuntimeException::new); // technical error
List<Foo> minFoos = foos.stream()
.filter(f -> f.getVariableCount() == min)
.collect(Collectors.toList());
Ответ 5
Чтобы избежать создания всей карты, а также избегая потоковой передачи дважды, я скопировал пользовательский сборник fooobar.com/questions/93157/... и изменил его для работы с min вместо max. Я даже не знал, что пользовательские коллекционеры были возможны, поэтому я благодарю @lexicore за то, что указал мне в этом направлении.
Это результирующая функция minAll
public static <T, A, D> Collector<T, ?, D> minAll(Comparator<? super T> comparator,
Collector<? super T, A, D> downstream) {
Supplier<A> downstreamSupplier = downstream.supplier();
BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
BinaryOperator<A> downstreamCombiner = downstream.combiner();
class Container {
A acc;
T obj;
boolean hasAny;
Container(A acc) {
this.acc = acc;
}
}
Supplier<Container> supplier = () -> new Container(downstreamSupplier.get());
BiConsumer<Container, T> accumulator = (acc, t) -> {
if(!acc.hasAny) {
downstreamAccumulator.accept(acc.acc, t);
acc.obj = t;
acc.hasAny = true;
} else {
int cmp = comparator.compare(t, acc.obj);
if (cmp < 0) {
acc.acc = downstreamSupplier.get();
acc.obj = t;
}
if (cmp <= 0)
downstreamAccumulator.accept(acc.acc, t);
}
};
BinaryOperator<Container> combiner = (acc1, acc2) -> {
if (!acc2.hasAny) {
return acc1;
}
if (!acc1.hasAny) {
return acc2;
}
int cmp = comparator.compare(acc1.obj, acc2.obj);
if (cmp < 0) {
return acc1;
}
if (cmp > 0) {
return acc2;
}
acc1.acc = downstreamCombiner.apply(acc1.acc, acc2.acc);
return acc1;
};
Function<Container, D> finisher = acc -> downstream.finisher().apply(acc.acc);
return Collector.of(supplier, accumulator, combiner, finisher);
}
Ответ 6
Здесь есть альтернатива одному потоку и пользовательскому редуктору. Идея состоит в том, чтобы сначала отсортировать, а затем собрать только элементы с первым минимальным значением:
List<Foo> newlist = list.stream()
.sorted( Comparator.comparing(Foo::getVariableCount) )
.reduce( new ArrayList<>(),
(l, f) -> {
if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f);
return l;
},
(l1, l2) -> {
l1.addAll(l2);
return l1;
}
);
Или использование сбора еще более компактно:
List<Foo> newlist = list.stream()
.sorted( Comparator.comparing(Foo::getVariableCount) )
.collect( ArrayList::new,
(l, f) -> if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f),
List::addAll
);
Ответ 7
Вы можете использовать разумный collect
в отсортированном списке, а в накопителе добавить логику, чтобы добавить только первый элемент в пустой список или добавить любой другой Foo, имеющий счетчик переменных, такой же, как и для первого элемента списка.
Полный рабочий пример ниже:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
class Foo {
private int variableCount;
public Foo(int vars) {
this.variableCount = vars;
}
public Integer getVariableCount() {
return variableCount;
}
public static void main(String[] args) {
List<Foo> list = Arrays.asList(
new Foo(2),
new Foo(2),
new Foo(3),
new Foo(3),
new Foo(1),
new Foo(1)
);
System.out.println(list.stream()
.sorted(Comparator.comparing(Foo::getVariableCount))
.collect(() -> new ArrayList<Foo>(),
(ArrayList<Foo> arrayList, Foo e) -> {
if (arrayList.isEmpty()
|| arrayList.get(0).getVariableCount() == e.getVariableCount()) {
arrayList.add(e);
}
},
(ArrayList<Foo> foos, ArrayList<Foo> foo) -> foos.addAll(foo)
)
);
}
@Override
public String toString() {
return "Foo{" +
"variableCount=" + variableCount +
'}';
}
}
Кроме того, вы можете сначала найти минимальный variableCount
в одном потоке и использовать этот внутренний фильтр другого потока.
list.sort(Comparator.comparing(Foo::getVariableCount));
int min = list.get(0).getVariableCount();
list.stream().filter(foo -> foo.getVariableCount() == min)
.collect(Collectors.toList());
Я думаю, что в любом случае требуется либо сортировка, либо способ найти минимальное число, которое позже может быть использовано внутри предиката. Даже если вы используете карту для группировки значений.
Ура!