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ę?


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.


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.


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

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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.