Вопрос-ответ

How to force max to return ALL maximum values in a Java Stream?

Как заставить max возвращать ВСЕ максимальные значения в потоке Java?

Я немного протестировал max функцию в Java 8 для лямбд и потоков, и кажется, что в случае выполнения max, даже если более одного объекта сравниваются с 0, он возвращает произвольный элемент в связанных кандидатах без дальнейшего рассмотрения.

Есть ли очевидный трюк или функция для такого максимального ожидаемого поведения, чтобы возвращались все максимальные значения? Я ничего не вижу в API, но я уверен, что должно существовать что-то лучше, чем сравнение вручную.

Например:

// myComparator is an IntegerComparator
Stream.of(1, 3, 5, 3, 2, 3, 5)
.max(myComparator)
.forEach(System.out::println);
// Would print 5, 5 in any order.
Переведено автоматически
Ответ 1

Я полагаю, что OP использует Comparator для разделения входных данных на классы эквивалентности, и желаемым результатом является список членов класса эквивалентности, который является максимальным в соответствии с этим Comparator.

К сожалению, использование int значений в качестве примера проблемы - ужасный пример. Все равные int значения взаимозаменяемы, поэтому нет понятия сохранения порядка эквивалентных значений. Возможно, лучшим примером является использование string lengths , где желаемым результатом является возврат списка строк из входных данных, все из которых имеют наибольшую длину в пределах этого ввода.

Я не знаю ни одного способа сделать это без сохранения хотя бы частичных результатов в коллекции.

Учитывая входную коллекцию, скажем

List<String> list = ... ;

...это достаточно просто сделать за два прохода, первый для получения наибольшей длины, а второй для фильтрации строк с такой длиной:

int longest = list.stream()
.mapToInt(String::length)
.max()
.orElse(-1);

List<String> result = list.stream()
.filter(s -> s.length() == longest)
.collect(toList());

Если входными данными является поток, который нельзя обойти более одного раза, результат можно вычислить только за один проход с помощью сборщика. Написать такой сборщик несложно, но это немного утомительно, поскольку необходимо обработать несколько случаев. Вспомогательная функция, которая генерирует такой сборщик, учитывая Comparator, выглядит следующим образом:

static <T> Collector<T,?,List<T>> maxList(Comparator<? super T> comp) {
return Collector.of(
ArrayList::new,
(list, t) -> {
int c;
if (list.isEmpty() || (c = comp.compare(t, list.get(0))) == 0) {
list.add(t);
} else if (c > 0) {
list.clear();
list.add(t);
}
},
(list1, list2) -> {
if (list1.isEmpty()) {
return list2;
}
if (list2.isEmpty()) {
return list1;
}
int r = comp.compare(list1.get(0), list2.get(0));
if (r < 0) {
return list2;
} else if (r > 0) {
return list1;
} else {
list1.addAll(list2);
return list1;
}
});
}

Промежуточные результаты сохраняются в ArrayList. Неизменным является то, что все элементы в любом таком списке эквивалентны с точки зрения Comparator. При добавлении элемента, если он меньше элементов в списке, он игнорируется; если он равен, он добавляется; и если он больше, список очищается и добавляется новый элемент. Объединение также не слишком сложно: возвращается список с большим количеством элементов, но если их элементы равны, списки добавляются.

Учитывая входной поток, это довольно просто в использовании:

Stream<String> input = ... ;

List<String> result = input.collect(maxList(comparing(String::length)));
Ответ 2

Я бы сгруппировал по значению и сохранил значения в a TreeMap чтобы отсортировать мои значения, тогда я бы получил максимальное значение, получив последнюю запись в качестве следующей:

Stream.of(1, 3, 5, 3, 2, 3, 5)
.collect(groupingBy(Function.identity(), TreeMap::new, toList()))
.lastEntry()
.getValue()
.forEach(System.out::println);

Вывод:

5
5
Ответ 3

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

public static <T, A, D> Collector<T, ?, D> maxAll(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);
}

Таким образом, по умолчанию он может быть собран в список с помощью:

public static <T> Collector<T, ?, List<T>> maxAll(Comparator<? super T> comparator) {
return maxAll(comparator, Collectors.toList());
}

Но вы можете использовать и другие нижестоящие сборщики:

public static String joinLongestStrings(Collection<String> input) {
return input.stream().collect(
maxAll(Comparator.comparingInt(String::length), Collectors.joining(","))));
}
Ответ 4

Если я правильно понял, вам нужна частота max значения в потоке.

Одним из способов добиться этого было бы сохранить результаты в TreeMap<Integer, List<Integer> когда вы собираете элементы из потока. Затем вы берете последний ключ (или первый, в зависимости от предоставленного вами компаратора), чтобы получить значение, которое будет содержать список максимальных значений.

List<Integer> maxValues = st.collect(toMap(i -> i,
Arrays::asList,
(l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(toList()),
TreeMap::new))
.lastEntry()
.getValue();

Собрав его из Stream(4, 5, -2, 5, 5), вы получитеList [5, 5, 5].

Другим подходом в том же духе было бы использовать операцию group by в сочетании с counting() collector:

Entry<Integer, Long> maxValues = st.collect(groupingBy(i -> i,
TreeMap::new,
counting())).lastEntry(); //5=3 -> 5 appears 3 times

По сути, сначала вы получаете Map<Integer, List<Integer>>. Затем нисходящий counting() сборщик вернет количество элементов в каждом списке, сопоставленных по его ключу, что приведет к отображению. Оттуда вы берете запись max.

Первые подходы требуют сохранения всех элементов из потока. Второй подход лучше (см. Комментарий Хольгера), поскольку промежуточный List не создан. В обоих подходах результат вычисляется за один проход.

Если вы получаете исходный код из коллекции, вы можете использовать Collections.max один раз для поиска максимального значения, за которым следует Collections.frequency, чтобы узнать, сколько раз это значение появляется.

Это требует двух проходов, но использует меньше памяти, поскольку вам не нужно создавать структуру данных.

Эквивалент потока будет coll.stream().max(...).get(...) сопровождаться coll.stream().filter(...).count().

java collections lambda java-8 java-stream