Wie verwende ich die neue Funktion computeIfAbsent?

115

Ich möchte Map.computeIfAbsent sehr gerne verwenden, aber es ist zu lange her, dass Lambdas im Undergrad sind.

Fast direkt aus den Dokumenten: Es gibt ein Beispiel für die alte Art, Dinge zu tun:

Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
String key = "snoop";
if (whoLetDogsOut.get(key) == null) {
  Boolean isLetOut = tryToLetOut(key);
  if (isLetOut != null)
    map.putIfAbsent(key, isLetOut);
}

Und der neue Weg:

map.computeIfAbsent(key, k -> new Value(f(k)));

Aber in ihrem Beispiel denke ich, dass ich es nicht ganz "verstehe". Wie würde ich den Code transformieren, um die neue Lambda-Methode zu verwenden, um dies auszudrücken?

Benjamin H.
quelle
Ich bin mir nicht sicher, was Sie aus dem Beispiel dort nicht verstehen?
Louis Wasserman
2
Was ist "k"? Wird eine Variable definiert? Wie wäre es mit "neuer Wert" - ist das etwas aus Java 8 oder repräsentiert es ein Objekt, das ich definieren oder überschreiben muss? whoLetDogsOut.computeIfAbsent (Schlüssel, k -> neuer Boolescher Wert (tryToLetOut (k))) wird nicht kompiliert, daher fehlt mir etwas ...
Benjamin H
Was genau kompiliert nicht? Welchen Fehler erzeugt es?
Axtavt
Temp.java:26: Fehler: unzulässiger Beginn des Ausdrucks whoLetDogsOut.computeIfAbsent (Schlüssel, k -> neuer Boolescher Wert (tryToLetOut (k))); (zeigt auf das ">")
Benjamin H
Kompiliert gut für mich. Stellen Sie sicher, dass Sie den Java 8-Compiler wirklich verwenden. Funktionieren andere Java 8-Funktionen?
Axtavt

Antworten:

97

Angenommen, Sie haben den folgenden Code:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] s) {
        Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
    }
    static boolean f(String s) {
        System.out.println("creating a value for \""+s+'"');
        return s.isEmpty();
    }
}

Dann sehen Sie die Nachricht creating a value for "snoop"genau einmal, da beim zweiten Aufruf von computeIfAbsentbereits ein Wert für diesen Schlüssel vorhanden ist. Der kAusdruck im Lambda k -> f(k)ist nur ein Platzhalter (Parameter) für den Schlüssel, den die Karte zur Berechnung des Werts an Ihr Lambda übergibt. Im Beispiel wird der Schlüssel also an den Funktionsaufruf übergeben.

Alternativ könnten Sie schreiben: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());um das gleiche Ergebnis ohne eine Hilfsmethode zu erzielen (aber die Debugging-Ausgabe wird dann nicht angezeigt). Und noch einfacher, da es sich um eine einfache Delegierung an eine vorhandene Methode handelt, die Sie schreiben könnten: Für whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);diese Delegierung müssen keine Parameter geschrieben werden.

Um näher an dem Beispiel in Ihrer Frage zu sein, können Sie es als schreiben whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(es spielt keine Rolle, ob Sie den Parameter benennen koder key). Oder schreiben Sie es als whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);ob tryToLetOutist staticoder whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);wenn tryToLetOuteine Instanz - Methode.

Holger
quelle
114

Kürzlich habe ich auch mit dieser Methode gespielt. Ich habe einen gespeicherten Algorithmus geschrieben, um Fibonacci-Zahlen zu berechnen, der als weiteres Beispiel für die Verwendung der Methode dienen könnte.

Wir können beginnen, indem wir eine Karte definieren und die Werte für die Basisfälle darin einfügen, nämlich fibonnaci(0)und fibonacci(1):

private static Map<Integer,Long> memo = new HashMap<>();
static {
   memo.put(0,0L); //fibonacci(0)
   memo.put(1,1L); //fibonacci(1)
}

Und für den induktiven Schritt müssen wir lediglich unsere Fibonacci-Funktion wie folgt neu definieren:

public static long fibonacci(int x) {
   return memo.computeIfAbsent(x, n -> fibonacci(n-2) + fibonacci(n-1));
}

Wie Sie sehen können, verwendet die Methode computeIfAbsentden bereitgestellten Lambda-Ausdruck, um die Fibonacci-Zahl zu berechnen, wenn die Zahl nicht in der Karte vorhanden ist. Dies stellt eine signifikante Verbesserung gegenüber dem herkömmlichen rekursiven Baumalgorithmus dar.

Edwin Dalorzo
quelle
18
Schöne einzeilige Konvertierung in dynamische Programmierung. Sehr glatt.
Benjamin H
3
Sie erhalten möglicherweise weniger rekursive Anrufe, wenn Sie zuerst den (n-2) -Aufruf haben?
Thorbjørn Ravn Andersen
9
Sie sollten vorsichtiger sein, wenn Sie computeIfAbsent rekursiv verwenden. Weitere Informationen finden Sie unter stackoverflow.com/questions/28840047/…
Ajit Kumar
11
Dieser Code führt dazu HashMap, dass die Interna beschädigt werden, genau wie in bugs.openjdk.java.net/browse/JDK-8172951, und schlägtConcurrentModificationException in Java 9 fehl ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
22
Die Dokumente sagen buchstäblich, dass die Zuordnungsfunktion diese Zuordnung während der Berechnung nicht ändern sollte , daher ist diese Antwort eindeutig falsch.
fps
41

Ein anderes Beispiel. Beim Erstellen einer komplexen Karte von Karten ersetzt die Methode computeIfAbsent () die get () -Methode der Karte. Durch Verketten von computeIfAbsent () -Aufrufen werden fehlende Container im laufenden Betrieb durch bereitgestellte Lambda-Ausdrücke erstellt:

  // Stores regional movie ratings
  Map<String, Map<Integer, Set<String>>> regionalMovieRatings = new TreeMap<>();

  // This will throw NullPointerException!
  regionalMovieRatings.get("New York").get(5).add("Boyhood");

  // This will work
  regionalMovieRatings
    .computeIfAbsent("New York", region -> new TreeMap<>())
    .computeIfAbsent(5, rating -> new TreeSet<>())
    .add("Boyhood");
hexabc
quelle
31

Multi-Map

Dies ist sehr hilfreich, wenn Sie eine Multimap erstellen möchten, ohne auf die Google Guava- Bibliothek zurückgreifen zu müssen MultiMap.

Angenommen, Sie möchten eine Liste der Schüler speichern, die sich für ein bestimmtes Fach eingeschrieben haben.

Die normale Lösung für die Verwendung der JDK-Bibliothek lautet:

Map<String,List<String>> studentListSubjectWise = new TreeMap<>();
List<String>lis = studentListSubjectWise.get("a");
if(lis == null) {
    lis = new ArrayList<>();
}
lis.add("John");

//continue....

Da es einen Boilerplate-Code gibt, neigen die Leute dazu, Guava zu verwenden Mutltimap.

Mit Map.computeIfAbsent können wir wie folgt in einer einzelnen Zeile ohne Guave Multimap schreiben.

studentListSubjectWise.computeIfAbsent("a", (x -> new ArrayList<>())).add("John");

Stuart Marks & Brian Goetz haben einen guten Vortrag über diese https://www.youtube.com/watch?v=9uTVXxJjuco gehalten

nantitv
quelle
Eine andere Möglichkeit, eine Multimap in Java 8 (und präziser) zu erstellen, besteht darin, dies einfach zu tun. studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());Dies erzeugt eine Multi-Map vom Typ Map<T,List<T>in JDK nur präziser imho.
Zombies