web-dev-qa-db-de.com

Java Stream: Finden Sie ein Element mit einem Min/Max-Wert eines Attributs

Ich habe einen Strom von Objekten und ich würde gerne das Objekt mit dem maximalen Wert eines Attributs finden, dessen Berechnung teuer ist.

Als ein einfaches einfaches Beispiel sagen wir, dass wir eine Liste von Strings haben und mit der coolnessIndex-Funktion den coolsten finden möchten.

Folgendes sollte funktionieren:

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

Nun gibt es zwei Probleme damit. Vorausgesetzt, dass die coolnessIndex teuer zu berechnen ist, ist dies wahrscheinlich nicht sehr effizient. Ich nehme an, die max-Methode muss den Komparator wiederholt verwenden, was wiederum die coolnessIndex wiederholt aufruft und am Ende mehrmals für jeden String aufgerufen wird.

Zweitens führt das Vorsehen des Komparators zu einer gewissen Redundanz im Code. Ich würde die Syntax so bevorzugen:

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

In der Stream-API konnte ich jedoch keine passende Methode finden. Dies überrascht mich, da das Ermitteln von min/max anhand eines Attributs wie ein allgemeines Muster erscheint. Ich frage mich, ob es einen besseren Weg gibt als den Vergleicher (außer einer for-Schleife).

19
Jan Pomikálek

Vielen Dank an alle für Vorschläge. Endlich fand ich die Lösung, die mir am besten gefällt: Effizienz der Funktionsweise von Comparator - die Antwort von bayou.io:

Verwenden Sie eine cache-Methode für allgemeine Zwecke:

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<>());
}

Dies könnte dann wie folgt verwendet werden:

String coolestString = stringList
        .stream()
        .max(Comparator.comparing(cache(CoolUtil::coolnessIndex)))
        .orElse(null);
2
Jan Pomikálek

Hier ist eine Variante, die einen Object[] als Tupel verwendet, nicht den schönsten Code, sondern knapp

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

Wie wäre es mit zwei Streams, einem, um eine Karte mit den vorberechneten Werten zu erstellen, und einem zweiten, der den Eintragssatz der Karte verwendet, um den maximalen Wert zu ermitteln:

        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

Ich würde eine lokale Klasse erstellen (eine Klasse, die in einer Methode definiert ist - selten, aber absolut legal) und Ihre Objekte darauf abbilden, sodass das teure Attribut genau einmal berechnet wird:

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

Dies ist ein Reduktionsproblem. Eine Liste auf einen bestimmten Wert reduzieren. Im Allgemeinen reduzieren Sie die Liste für eine Teillösung und einen Eintrag in der Liste. In diesem Fall würde dies bedeuten, dass der vorherige "Gewinn" -Wert mit dem neuen Wert aus der Liste verglichen wird, wodurch die teure Operation bei jedem Vergleich zweimal berechnet wird.

Gemäß https://docs.Oracle.com/javase/tutorial/collections/streams/reduction.html ist eine Alternative die Verwendung von Collect anstelle von Reduce.

Eine benutzerdefinierte consumer class ermöglicht es Ihnen, die kostspieligen Vorgänge zu verfolgen, da sie die Liste reduziert. Verbraucher können die mehrfachen Aufrufe der teuren Berechnung umgehen, indem sie mit dem veränderlichen Status arbeiten.

    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);
    }
}

Diese Klasse akzeptiert eine Zeichenfolge. Wenn sie kühler ist als der vorherige Gewinner, ersetzt sie diese und speichert den teuren berechneten Wert.

Cooler cooler =  Stream.of("Java", "php", "clojure", "c", "LISP")
                 .collect(Cooler::new, Cooler::accept, Cooler::combine);
System.out.println(cooler.coolest());
0
GregA100k

erstellen Sie zuerst Ihre (Objekt-, Metrik-) Paare:

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);
}

(diese sind com.googlecode.totallylazy.Pair's)

0
Paul Janssens

Sie können die Idee von Sammeln der Ergebnisse des Streams entsprechend nutzen. Aufgrund der Einschränkung der Funktion für die kostenintensive Coolness-Funktion sollten Sie die Funktion für jedes Element des Streams genau einmal aufrufen.

Java 8 bietet die collect-Methode für Stream und verschiedene Möglichkeiten, Kollektoren zu verwenden. Es scheint, dass Sie, wenn Sie die TreeMap zum Sammeln Ihrer Ergebnisse verwendet haben, die Ausdruckskraft behalten und gleichzeitig rücksichtsvoll auf die Effizienz achten können:

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;
    }
}

Dieser Code druckt die stooge mit maximaler Coolness und die coolness-Methode wird genau einmal für jede stooge-Methode aufgerufen. Das BinaryOperator, das als mergeFunction ((a, b) ->a) arbeitet, kann weiter verbessert werden. 

0
Kedar Mhaswade