Использование потоковых методов 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
Удалите опцию равенства (не возвращайте 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);