Использование потоковых методов Java 8 для получения последнего максимального значения - PullRequest
0 голосов
/ 05 февраля 2019

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

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

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. Не используйте потоки

Ответы [ 7 ]

0 голосов
/ 05 февраля 2019

Поток не обязательно плох, если вы делаете вещи в два шага:

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

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)
0 голосов
/ 05 февраля 2019

Функция 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);
0 голосов
/ 05 февраля 2019

Чтобы избежать многократного применения 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);
0 голосов
/ 05 февраля 2019

Ваша текущая реализация с использованием 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);
0 голосов
/ 05 февраля 2019

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

Thing t = items.stream()
        .max(Comparator.comparingLong(Thing::getI).thenComparing(items::indexOf))
        .orElse(null);
0 голосов
/ 05 февраля 2019

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

Thing t = items.stream()
        .max((a, b) -> a.getI() > b.getI() ? 1 : -1)
        .orElse(null);
0 голосов
/ 05 февраля 2019

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

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