Использование потоковых методов Java 8 для получения последнего максимального значения

Учитывая список элементов со свойствами, я пытаюсь получить последний элемент с максимальным значением указанного свойства.

Например, для следующего списка объектов:

t  i
A: 3
D: 7 *
F: 4
C: 5
X: 7 *
M: 6

Я могу получить одну из вещей с самым высоким i:

Thing t = items.stream()
        .max(Comparator.comparingLong(Thing::getI))
        .orElse(null);

Тем не менее, это даст мне Thing t = D Есть ли чистый и элегантный способ получить последний элемент, то есть X в этом случае?

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

Thing t = items.stream()
        .reduce((left, right) -> {
            long leftValue = valueFunction.apply(left);
            long rightValue = valueFunction.apply(right);
            return leftValue > rightValue ? left : right;
        })
        .orElse(null);

valueFunction теперь нужно вызывать почти вдвое чаще.

Другие очевидные обходные решения:

  1. Храните объект в кортеже с его индексом
  2. Сохраните объект в кортеже с его вычисленным значением
  3. Переверните список заранее
  4. Не используйте потоки

Ответы

Ответ 1

Удалите опцию равенства (не возвращайте 0, если сравниваемые числа равны, вместо этого верните -1) из компаратора (то есть напишите свой собственный компаратор, который не включает опцию равенства):

Thing t = items.stream()
        .max((a, b) -> a.getI() > b.getI() ? 1 : -1)
        .orElse(null);

Ответ 2

Концептуально, вы, возможно, ищете что-то вроде thenComparing используя index элементов в списке:

Thing t = items.stream()
        .max(Comparator.comparingLong(Thing::getI).thenComparing(items::indexOf))
        .orElse(null);

Ответ 3

Чтобы избежать множественных применений valueFunction в вашем решении для сокращения, просто явно вычислите результат и поместите его в кортеж:

Item lastMax = items.stream()
        .map(item -> new AbstractMap.SimpleEntry<Item, Long>(item, valueFunction.apply(item)))
        .reduce((l, r) -> l.getValue() > r.getValue() ? l : r )
        .map(Map.Entry::getKey)
        .orElse(null);

Ответ 4

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

Thing t = items.stream().
    reduce((t1, t2) -> t1.getI() > t2.getI() ? t1 : t2)
    .orElse(null);

Ответ 5

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

Учитывая, что позже вы захотите повторно использовать логику для различных типов объектов и полей, я бы выделил саму логику в виде отдельного универсального метода/методов:

public static <T, K, V> Function<T, Map.Entry<K, V>> toEntry(Function<T, K> keyFunc, Function<T, V> valueFunc){
    return t -> new AbstractMap.SimpleEntry<>(keyFunc.apply(t), valueFunc.apply(t));
}

public static <ITEM, FIELD extends Comparable<FIELD>> Optional<ITEM> maxBy(Function<ITEM, FIELD> extractor, Collection<ITEM> items) {
    return items.stream()
                .map(toEntry(identity(), extractor))
                .max(comparing(Map.Entry::getValue))
                .map(Map.Entry::getKey);
}

Приведенные выше фрагменты кода можно использовать так:

Thing maxThing =  maxBy(Thing::getField, things).orElse(null);

AnotherThing maxAnotherThing = maxBy(AnotherThing::getAnotherField, anotherThings).orElse(null);

Ответ 6

Поток не нужен плохо, если вы делаете вещи в два этапа:

1) Найдите значение i которое имеет больше вхождений в Iterable (как вы это сделали)
2) Поиск последнего элемента для этого значения i, начиная с конца элементов:

Thing t =  
  items.stream()
        .max(Comparator.comparingLong(Thing::getI))
        .mapping(firstMaxThing ->  
                   return
                   IntStream.rangeClosed(1, items.size())
                            .mapToObj(i -> items.get(items.size()-i))
                            .filter(item -> item.getI() == firstMaxThing.getI())
                            .findFirst().get(); 
                            // here get() cannot fail as *max()* returned something.
         )
       .orElse(null)

Ответ 7

ValueFunction теперь нужно вызывать почти вдвое чаще.

Обратите внимание, что даже при использовании max метод getI будет вызываться снова и снова для каждого сравнения, а не только для каждого элемента. В вашем примере он вызывается 11 раз, в том числе 6 раз для D, и для более длинных списков он также вызывается в среднем дважды на элемент.

Как насчет того, чтобы просто кэшировать вычисленное значение непосредственно в экземпляре Thing? Если это невозможно, вы можете использовать внешнюю Map и использовать calculateIfAbsent чтобы рассчитать значение только один раз для каждой Thing а затем использовать свой подход с использованием метода reduce.

Map<Thing, Long> cache = new HashMap<>();
Thing x = items.stream()
        .reduce((left, right) -> {
            long leftValue = cache.computeIfAbsent(left, Thing::getI);
            long rightValue = cache.computeIfAbsent(right, Thing::getI);
            return leftValue > rightValue ? left : right;
        })
        .orElse(null);

Или немного чище, вычисляя все значения заранее:

Map<Thing, Long> cache = items.stream()
        .collect(Collectors.toMap(x -> x, Thing::getI));
Thing x = items.stream()
        .reduce((left, right) -> cache.get(left) > cache.get(right) ? left : right)
        .orElse(null);

Ответ 8

Другой вариант - кэширование рассчитанных значений на Map а затем использование кеша в компараторе:

public static <ITEM, FIELD extends Comparable<FIELD>> Optional<ITEM> maxBy(Function<ITEM, FIELD> extractor, Collection<ITEM> items) {
    Map<ITEM, FIELD> cache = items.stream().collect(toMap(identity(), extractor));
    return items.stream().max(comparing(cache::get));
}

Thing t = maxBy(Thing::getI, items);