Как заставить 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 использует Компаратор для разбиения ввода на классы эквивалентности, а желаемый результат - список членов класса эквивалентности, который является максимальным в соответствии с этим Компаратором.

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

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

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

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

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

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

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

Stream<String> input = ... ;

List<String> result = input.collect(maxList(comparing(String::length)));

Ответ 2

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

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(","))));
}

Ответ 3

Если я хорошо понял, вам нужна частота значения 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].

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

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

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

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

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

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

эквивалент потока будет coll.stream().max(...).get(...), за которым следует coll.stream().filter(...).count().

Ответ 4

Я не уверен, пытаетесь ли вы

  • (a) найти количество вхождений максимального элемента или
  • (b) Найдите все максимальные значения в случае a Comparator, которые не соответствуют equals.

Примером (a) будет [1, 5, 4, 5, 1, 1] -> [5, 5].

Примером (b) может быть:

Stream.of("Bar", "FOO", "foo", "BAR", "Foo")
      .max((s, t) -> s.toLowerCase().compareTo(t.toLowerCase()));

который вы хотите дать [Foo, foo, Foo], а не просто FOO или Optional[FOO].

В обоих случаях есть умные способы сделать это за один проход. Но эти подходы имеют сомнительную ценность, потому что вам нужно будет отслеживать ненужную информацию на этом пути. Например, если вы начинаете с [2, 0, 2, 2, 1, 6, 2], это будет только когда вы достигнете 6, что вы понимаете, что нет необходимости отслеживать все 2 s.

Я думаю, что лучший подход является очевидным; используйте max, а затем повторите пункты, помещая все связи в коллекцию по вашему выбору. Это будет работать как для (a), так и для (b).

Ответ 5

Я бы сделал это так:

// I group by value and put it into a TreeMap then get the max value
Stream.of(1,3,5,3,2,3,5).collect(groupingBy(Function.identity(), TreeMap::new, toList()))
    .lastEntry()
    .getValue()
    .forEach(System.out::println);

Вывод:

5
5