Comment utiliser la nouvelle fonction computeIfAbsent?

115

Je veux vraiment utiliser Map.computeIfAbsent mais cela fait trop longtemps depuis les lambdas au premier cycle.

Presque directement à partir de la documentation: cela donne un exemple de l'ancienne façon de faire les choses:

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

Et la nouvelle façon:

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

Mais dans leur exemple, je pense que je ne «comprends» pas tout à fait. Comment transformer le code pour utiliser la nouvelle manière lambda d'exprimer cela?

Benjamin H
la source
Je ne suis pas sûr de ce que vous ne comprenez pas dans l'exemple là-bas?
Louis Wasserman
2
Qu'est-ce que "k"? Est-ce une variable en cours de définition? Que diriez-vous de "nouvelle valeur" - est-ce quelque chose de Java 8, ou représentant un objet que je dois définir ou remplacer? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) ne compile pas, donc il me manque quelque chose ...
Benjamin H
Qu'est-ce qui ne compile pas exactement? Quelle erreur cela produit-il?
axtavt
Temp.java:26: erreur: début incorrect de l'expression whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (indiquant le ">")
Benjamin H
Compile bien pour moi. Assurez-vous que vous utilisez vraiment le compilateur Java 8. Les autres fonctionnalités de Java 8 fonctionnent-elles?
axtavt

Réponses:

96

Supposons que vous ayez le code suivant:

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

Ensuite, vous verrez le message creating a value for "snoop"exactement une fois, car lors de la deuxième invocation, computeIfAbsentil y a déjà une valeur pour cette clé. Le kdans l'expression lambda k -> f(k)est juste un emplacement (paramètre) pour la clé que la carte passera à votre lambda pour calculer la valeur. Ainsi, dans l'exemple, la clé est transmise à l'appel de la fonction.

Vous pouvez également écrire: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());pour obtenir le même résultat sans méthode d'assistance (mais vous ne verrez alors pas la sortie de débogage). Et encore plus simple, car il s'agit d'une simple délégation à une méthode existante que vous pourriez écrire: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);Cette délégation n'a pas besoin d' écrire de paramètres.

Pour être plus proche de l'exemple de votre question, vous pouvez l'écrire comme whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(peu importe si vous nommez le paramètre kou key). Ou écrivez-le comme whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);si tryToLetOutest staticou whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);si tryToLetOutest une méthode d'instance.

Holger
la source
114

Récemment, je jouais aussi avec cette méthode. J'ai écrit un algorithme mémorisé pour calculer les nombres de Fibonacci qui pourrait servir d'illustration supplémentaire sur la façon d'utiliser la méthode.

Nous pouvons commencer par définir une carte et y mettre les valeurs pour les cas de base, à savoir, fibonnaci(0)et fibonacci(1):

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

Et pour l'étape inductive, tout ce que nous avons à faire est de redéfinir notre fonction de Fibonacci comme suit:

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

Comme vous pouvez le voir, la méthode computeIfAbsentutilisera l'expression lambda fournie pour calculer le nombre de Fibonacci lorsque le nombre n'est pas présent dans la carte. Cela représente une amélioration significative par rapport à l'algorithme récursif traditionnel.

Edwin Dalorzo
la source
18
Belle conversion sur une seule ligne en programmation dynamique. Très lisse.
Benjamin H
3
Vous pouvez obtenir moins d'appels récursifs si vous avez d'abord l'appel (n-2)?
Thorbjørn Ravn Andersen
10
Vous devez être plus prudent lorsque vous utilisez computeIfAbsent de manière récursive. Pour plus de détails, veuillez consulter stackoverflow.com/questions/28840047/…
Ajit Kumar
12
Ce code entraîne HashMapla corruption des composants internes de 's, tout comme dans bugs.openjdk.java.net/browse/JDK-8172951 et échouera avec ConcurrentModificationExceptiondans Java 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
23
La documentation dit littéralement que la fonction de mappage ne doit pas modifier cette carte pendant le calcul , donc cette réponse est clairement erronée.
fps
41

Un autre exemple. Lors de la construction d'une carte complexe de cartes, la méthode computeIfAbsent () remplace la méthode get () de la carte. Grâce au chaînage des appels computeIfAbsent (), les conteneurs manquants sont construits à la volée par les expressions lambda fournies:

  // 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
la source
31

multi-carte

Ceci est vraiment utile si vous souhaitez créer une multi - carte sans recourir à la bibliothèque Google Guava pour sa mise en œuvre de MultiMap.

Par exemple, supposons que vous souhaitiez stocker une liste d'étudiants qui se sont inscrits pour une matière particulière.

La solution normale pour cela en utilisant la bibliothèque JDK est:

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

//continue....

Puisqu'il a un code standard, les gens ont tendance à utiliser Guava Mutltimap.

En utilisant Map.computeIfAbsent, nous pouvons écrire sur une seule ligne sans goyave Multimap comme suit.

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

Stuart Marks et Brian Goetz ont fait une bonne discussion à ce sujet https://www.youtube.com/watch?v=9uTVXxJjuco

nantitv
la source
Une autre façon de faire une studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());multi-carte en Java 8 (et plus concise) est de simplement faire Cela produit une multi-carte de type Map<T,List<T>dans JDK de manière plus concise à mon humble avis.
Zombies