Подтвердить что ты не робот

Java Stream: найдите элемент с минимальным/максимальным значением атрибута

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

Как конкретный простой пример, скажем, что у нас есть список строк, и мы хотим найти самый крутой, учитывая функцию coolnessIndex.

Следующее должно работать:

String coolestString = stringList
        .stream()
        .max((s1, s2) -> Integer.compare(coolnessIndex(s1), coolnessIndex(s2)))
        .orElse(null);

Теперь у нас есть две проблемы. Во-первых, если предположить, что coolnessIndex дорого рассчитать, это, вероятно, будет не очень эффективным. Я полагаю, что метод max должен будет повторно использовать компаратор, который, в свою очередь, будет называть coolnessIndex несколько раз, а в конце он будет вызываться более одного раза для каждой строки.

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

String coolestString = stringList
        .stream()
        .maxByAttribute(s -> coolnessIndex(s))
        .orElse(null);

Однако мне не удалось найти подходящий метод в API Stream. Это меня удивляет, так как поиск min/max по атрибуту выглядит как общий шаблон. Интересно, есть ли лучший способ, чем использование компаратора (кроме цикла for).

4b9b3361

Ответ 1

Спасибо всем за предложения. Наконец, я нашел решение, которое мне больше всего нравится в Эффективность работы компаратора - ответ от bayou.io:

Использовать общий метод cache:

public static <K,V> Function<K,V> cache(Function<K,V> f, Map<K,V> cache)
{
    return k -> cache.computeIfAbsent(k, f);
}

public static <K,V> Function<K,V> cache(Function<K,V> f)
{
    return cache(f, new IdentityHashMap<>());
}

Затем это можно использовать следующим образом:

String coolestString = stringList
        .stream()
        .max(Comparator.comparing(cache(CoolUtil::coolnessIndex)))
        .orElse(null);

Ответ 2

Здесь вариант, использующий Object[] как кортеж, а не самый красивый код, но краткий

String coolestString = stringList
        .stream()
        .map(s -> new Object[] {s, coolnessIndex(s)})
        .max(Comparator.comparingInt(a -> (int)a[1]))
        .map(a -> (String)a[0])
        .orElse(null);

Ответ 3

Stream<String> stringStream = stringList.stream();
String coolest = stringStream.reduce((a,b)-> 
    coolnessIndex(a) > coolnessIndex(b) ? a:b;
).get()

Ответ 4

Как использовать два потока, один для создания карты с предварительно рассчитанными значениями, а второй - с помощью набора записей, чтобы найти максимальное значение:

        String coolestString = stringList
            .stream()
            .collect(Collectors.toMap(Function.identity(), Test::coolnessIndex))
            .entrySet()
            .stream()
            .max((s1, s2) -> Integer.compare(s1.getValue(), s2.getValue()))
            .orElse(null)
            .getKey();

Ответ 5

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

В соответствии с https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html альтернативой является использование сбора вместо сокращения.

Пользовательский consumer класс позволит отслеживать дорогостоящие операции по мере уменьшения списка. Потребитель может обойти несколько вызовов дорогостоящего расчета, работая с изменяемым состоянием.

    class Cooler implements Consumer<String>{

    String coolestString = "";
    int coolestValue = 0;

    public String coolest(){
        return coolestString;
    }
    @Override
    public void accept(String arg0) {
        combine(arg0, expensive(arg0));
    }

    private void combine (String other, int exp){
        if (coolestValue < exp){
            coolestString = other;
            coolestValue = exp;
        }
    }
    public void combine(Cooler other){
        combine(other.coolestString, other.coolestValue);
    }
}

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

Cooler cooler =  Stream.of("java", "php", "clojure", "c", "lisp")
                 .collect(Cooler::new, Cooler::accept, Cooler::combine);
System.out.println(cooler.coolest());

Ответ 6

Я бы создал локальный класс (класс, определенный внутри метода - редкий, но совершенно законный) и сопоставлял ваши объекты с этим, поэтому дорогостоящий атрибут вычисляется ровно один раз для каждого:

class IndexedString {
    final String string;
    final int index;

    IndexedString(String s) {
        this.string = Objects.requireNonNull(s);
        this.index = coolnessIndex(s);
    }

    String getString() {
        return string;
    }

    int getIndex() {
        return index;
    }
}

String coolestString = stringList
    .stream()
    .map(IndexedString::new)
    .max(Comparator.comparingInt(IndexedString::getIndex))
    .map(IndexedString::getString)
    .orElse(null);

Ответ 7

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

Java 8 предоставляет метод collect для Stream и множество способов использования коллекционеров. Похоже, что если вы использовали TreeMap для сбора ваших результатов, вы можете сохранить выразительность и в то же время оставаться внимательным к эффективности:

public class Expensive {
    static final Random r = new Random();
    public static void main(String[] args) {
        Map.Entry<Integer, String> e =
        Stream.of("larry", "moe", "curly", "iggy")
                .collect(Collectors.toMap(Expensive::coolness,
                                          Function.identity(),
                                          (a, b) -> a,
                                          () -> new TreeMap<>
                                          ((x, y) -> Integer.compare(y, x))
                        ))
                .firstEntry();
        System.out.println("coolest stooge name: " + e.getKey() + ", coolness: " + e.getValue());
    }

    public static int coolness(String s) {
        // simulation of a call that takes time.
        int x = r.nextInt(100);
        System.out.println(x);
        return x;
    }
}

Этот код печатает stooge с максимальной прохладой, а метод coolness вызывается ровно один раз для каждого stooge. BinaryOperator, который работает как mergeFunction ((a, b) ->a), может быть дополнительно улучшен.