it-roy-ru.com

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. Это удивляет меня, так как нахождение мин/макс по атрибуту похоже на общий шаблон. Интересно, есть ли лучший способ, чем использовать компаратор (кроме цикла for).

19
Jan Pomikálek

Спасибо всем за предложения. Наконец-то я нашел решение, которое мне больше всего нравится в Эффективность работы компаратора - ответ от 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
Jan Pomikálek

Вот вариант, использующий 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);
8
gustf
Stream<String> stringStream = stringList.stream();
String coolest = stringStream.reduce((a,b)-> 
    coolnessIndex(a) > coolnessIndex(b) ? a:b;
).get()
8
frhack

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

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

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

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);
0
VGR

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

Согласно https://docs.Oracle.com/javase/tutorial/collections/streams/reduction.html альтернативой является использование сбора вместо уменьшения.

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

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

сначала создайте свои пары (объект, метрика):

public static <T> Optional<T> maximizeOver(List<T> ts, Function<T,Integer> f) {
    return ts.stream().map(t -> Pair.pair(t, f.apply(t)))
        .max((p1,p2) -> Integer.compare(p1.second(), p2.second()))
        .map(Pair::first);
}

(это com.googlecode.totallylazy.Pair's)

0
Paul Janssens

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

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), может быть улучшен. 

0
Kedar Mhaswade