Jak używać nowej funkcji computeIfAbsent?

115

Bardzo chcę używać Map.computeIfAbsent, ale minęło zbyt dużo czasu od czasów lambda w undergrad.

Niemal bezpośrednio z dokumentacji: zawiera przykład starego sposobu robienia rzeczy:

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

I nowy sposób:

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

Ale w ich przykładzie wydaje mi się, że nie całkiem „rozumiem”. Jak powinienem przekształcić kod, aby używał nowego sposobu wyrażania tego przez lambdę?

Benjamin H.
źródło
Nie jestem pewien, czego nie rozumiesz z tamtego przykładu?
Louis Wasserman
2
Co to jest „k”? Czy jest to definiowana zmienna? A co z „nową wartością” - czy jest to coś z języka Java 8 lub reprezentujące obiekt, który muszę zdefiniować lub zastąpić? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) nie kompiluje się, więc czegoś mi brakuje ...
Benjamin H
Co dokładnie się nie kompiluje? Jaki błąd powoduje?
axtavt
Temp.java:26: błąd: niepoprawny początek wyrażenia whoLetDogsOut.computeIfAbsent (klucz, k -> new Boolean (tryToLetOut (k))); (wskazując na „>”)
Benjamin H,
Kompiluje się dobrze dla mnie. Upewnij się, że naprawdę używasz kompilatora Java 8. Czy działają inne funkcje Java 8?
axtavt

Odpowiedzi:

96

Załóżmy, że masz następujący kod:

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

Wtedy zobaczysz wiadomość creating a value for "snoop"dokładnie raz, ponieważ przy drugim wywołaniu computeIfAbsentjest już wartość dla tego klucza. W kwyrażeniu lambda k -> f(k)jest po prostu miejscem (parametr) dla klucza, który mapa przekaże do twojej lambdy w celu obliczenia wartości. W tym przykładzie klucz jest przekazywany do wywołania funkcji.

Alternatywnie możesz napisać: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());aby osiągnąć ten sam wynik bez metody pomocniczej (ale wtedy nie zobaczysz wyniku debugowania). A nawet prostsze, ponieważ jest to prosta delegacja do istniejącej metody, którą można napisać: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);Ta delegacja nie wymaga zapisywania żadnych parametrów.

Aby być bliżej przykładu w swoim pytaniu, możesz zapisać go jako whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(nie ma znaczenia, czy nazwiesz parametr, kczy key). Lub napisz to tak, whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);jakby tryToLetOutbyło staticlub whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);jeśli tryToLetOutjest metodą instancji.

Holger
źródło
114

Ostatnio też bawiłem się tą metodą. Napisałem zapamiętany algorytm do obliczania liczb Fibonacciego, który mógłby posłużyć jako kolejna ilustracja tego, jak korzystać z metody.

Możemy zacząć od zdefiniowania mapę i umieszczenie w nim wartości dla bazy przypadkach, a mianowicie, fibonnaci(0)i fibonacci(1):

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

A dla kroku indukcyjnego wszystko, co musimy zrobić, to przedefiniować naszą funkcję Fibonacciego w następujący sposób:

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

Jak widać, metoda computeIfAbsentwykorzysta podane wyrażenie lambda do obliczenia liczby Fibonacciego, gdy liczba ta nie jest obecna na mapie. Stanowi to znaczną poprawę w stosunku do tradycyjnego, rekurencyjnego algorytmu drzewiastego.

Edwin Dalorzo
źródło
18
Ładna, jednoliniowa konwersja do programowania dynamicznego. Bardzo zgrabny.
Benjamin H,
3
Możesz otrzymać mniej wywołań rekurencyjnych, jeśli najpierw masz wywołanie (n-2)?
Thorbjørn Ravn Andersen
10
Należy zachować większą ostrożność podczas rekurencyjnego korzystania z computeIfAbsent. Aby uzyskać więcej informacji, sprawdź stackoverflow.com/questions/28840047/…
Ajit Kumar
12
Ten kod powoduje HashMapuszkodzenie wewnętrznych elementów, podobnie jak w bugs.openjdk.java.net/browse/JDK-8172951 i zawiedzie ConcurrentModificationExceptionw Javie 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
23
Dokumentacja dosłownie mówi, że funkcja mapująca nie powinna modyfikować tej mapy podczas obliczeń , więc ta odpowiedź jest wyraźnie błędna.
fps
41

Inny przykład. Podczas budowania złożonej mapy map metoda computeIfAbsent () zastępuje metodę get () mapy. Poprzez łączenie wywołań computeIfAbsent () razem, brakujące kontenery są konstruowane w locie za pomocą podanych wyrażeń lambda:

  // 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
źródło
31

wiele map

Jest to bardzo pomocne, jeśli chcesz utworzyć multimapę bez uciekania się do biblioteki Google Guava w celu jej implementacji MultiMap.

Załóżmy na przykład, że chcesz przechowywać listę studentów, którzy zapisali się na określony przedmiot.

Normalnym rozwiązaniem tego problemu przy użyciu biblioteki JDK jest:

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

//continue....

Ponieważ ma jakiś standardowy kod, ludzie zwykle używają guawy Mutltimap.

Używając Map.computeIfAbsent, możemy pisać w jednej linii bez guawy Multimap w następujący sposób.

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

Stuart Marks i Brian Goetz zrobili dobrą rozmowę na ten temat https://www.youtube.com/watch?v=9uTVXxJjuco

nantitv
źródło
Innym sposobem na zrobienie multimapy w Javie 8 (i bardziej zwięzłym) jest zrobienie studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());tego po prostu. To tworzy multi-mapę typu Map<T,List<T>w JDK, tylko bardziej zwięźle imho.
Zombies