Come si usa la nuova funzione computeIfAbsent?


115

Vorrei davvero usare Map.computeIfAbsent ma è passato troppo tempo da quando lambda in undergrad.

Quasi direttamente dalla documentazione: fornisce un esempio del vecchio modo di fare le cose:

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

E il nuovo modo:

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

Ma nel loro esempio, penso di non "capirlo". Come trasformerei il codice per usare il nuovo modo lambda di esprimerlo?


Non sono sicuro di cosa non capisci dall'esempio qui?
Louis Wasserman

2
Cos'è "k"? È una variabile in fase di definizione? Che ne dici di "nuovo valore" - è qualcosa da Java 8 o rappresenta un oggetto che devo definire o sovrascrivere? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) non si compila, quindi mi manca qualcosa ...
Benjamin H

Cosa non viene compilato esattamente? Che errore produce?
axtavt

Temp.java:26: errore: inizio illegale dell'espressione whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (indicando ">")
Benjamin H

Compila bene per me. Assicurati di utilizzare davvero il compilatore Java 8. Altre funzionalità di Java 8 funzionano?
axtavt

Risposte:


96

Supponi di avere il seguente codice:

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

Quindi vedrai il messaggio creating a value for "snoop"esattamente una volta poiché alla seconda chiamata di computeIfAbsentc'è già un valore per quella chiave. L' kespressione lambda k -> f(k)è solo un segnaposto (parametro) per la chiave che la mappa passerà al tuo lambda per calcolare il valore. Quindi nell'esempio la chiave viene passata alla chiamata della funzione.

In alternativa puoi scrivere: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());per ottenere lo stesso risultato senza un metodo di supporto (ma allora non vedrai l'output di debug). E ancora più semplice, poiché è una semplice delega a un metodo esistente che potresti scrivere: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);questa delega non necessita di alcun parametro per essere scritta.

Per essere più vicino all'esempio nella tua domanda, potresti scriverlo come whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(non importa se chiami il parametro ko key). Oppure scrivilo come whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);se tryToLetOutfosse statico whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);se tryToLetOutfosse un metodo di istanza.


114

Recentemente stavo giocando anche con questo metodo. Ho scritto un algoritmo memorizzato per calcolare i numeri di Fibonacci che potrebbe servire come un'altra illustrazione su come usare il metodo.

Possiamo iniziare definendo una mappa e inserendovi i valori per i casi base, vale a dire, fibonnaci(0)e fibonacci(1):

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

E per il passo induttivo tutto ciò che dobbiamo fare è ridefinire la nostra funzione di Fibonacci come segue:

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

Come puoi vedere, il metodo computeIfAbsentutilizzerà l'espressione lambda fornita per calcolare il numero di Fibonacci quando il numero non è presente nella mappa. Ciò rappresenta un miglioramento significativo rispetto al tradizionale algoritmo ricorsivo ad albero.


18
Simpatica conversione di una riga alla programmazione dinamica. Molto lucido.
Benjamin H

3
Potresti ricevere meno chiamate ricorsive se hai prima la chiamata (n-2)?
Thorbjørn Ravn Andersen

10
Dovresti essere più cauto quando usi computeIfAbsent in modo ricorsivo. Per maggiori dettagli, visita
stackoverflow.com/questions/28840047/…

12
Questo codice provoca il HashMapdanneggiamento degli interni di, proprio come in bugs.openjdk.java.net/browse/JDK-8172951 e non funzionerà con ConcurrentModificationExceptionJava 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen

23
I documenti dicono letteralmente che la funzione di mappatura non dovrebbe modificare questa mappa durante il calcolo , quindi questa risposta è chiaramente sbagliata.
fps

41

Un altro esempio. Quando si costruisce una mappa complessa di mappe, il metodo computeIfAbsent () sostituisce il metodo get () di map. Attraverso il concatenamento di chiamate computeIfAbsent (), i contenitori mancanti vengono costruiti al volo dalle espressioni lambda fornite:

  // 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

multi-mappa

Questo è davvero utile se vuoi creare una mappa multipla senza ricorrere alla libreria Guava di Google per la sua implementazione di MultiMap.

Ad esempio, supponi di voler memorizzare un elenco di studenti che si sono iscritti per una materia particolare.

La soluzione normale per questo utilizzando la libreria JDK è:

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

//continue....

Dal momento che ha un codice boilerplate, le persone tendono a usare Guava Mutltimap.

Utilizzando Map.computeIfAbsent, possiamo scrivere in una singola riga senza guava Multimap come segue.

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

Stuart Marks e Brian Goetz hanno parlato bene di questo https://www.youtube.com/watch?v=9uTVXxJjuco


Un altro modo per creare una mappa multipla in Java 8 (e più conciso) è semplicemente farlo studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());Questo produce una mappa multipla di tipo Map<T,List<T>in JDK solo in modo più conciso imho.
Zombies
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.