¿Cómo utilizo la nueva función computeIfAbsent?

115

Tengo muchas ganas de usar Map.computeIfAbsent, pero ha pasado demasiado tiempo desde las lambdas en la licenciatura.

Casi directamente de los documentos: da un ejemplo de la forma antigua de hacer las cosas:

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

Y la nueva forma:

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

Pero en su ejemplo, creo que no lo estoy "entendiendo" del todo. ¿Cómo transformaría el código para usar la nueva forma lambda de expresar esto?

Benjamín H
fuente
No estoy seguro de qué es lo que no entiendes del ejemplo.
Louis Wasserman
2
¿Qué es "k"? ¿Es una variable que se está definiendo? ¿Qué tal un "valor nuevo"? ¿Es algo de Java 8 o representa un objeto que necesito definir o anular? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) no se compila, así que me falta algo ...
Benjamin H
¿Qué no se compila exactamente? ¿Qué error produce?
axtavt
Temp.java:26: error: inicio ilegal de la expresión whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (señalando el ">")
Benjamin H
Compila bien para mí. Asegúrese de utilizar realmente el compilador de Java 8. ¿Funcionan otras características de Java 8?
axtavt

Respuestas:

97

Suponga que tiene el siguiente código:

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

Luego verá el mensaje creating a value for "snoop"exactamente una vez, ya que en la segunda invocación computeIfAbsentya existe un valor para esa clave. En kla expresión lambda k -> f(k)es solo un marcador de posición (parámetro) para la clave que el mapa pasará a su lambda para calcular el valor. Entonces, en el ejemplo, la clave se pasa a la invocación de la función.

Alternativamente, podría escribir: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());para lograr el mismo resultado sin un método auxiliar (pero no verá la salida de depuración entonces). Y aún más simple, ya que es una simple delegación a un método existente que podría escribir: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);esta delegación no necesita ningún parámetro para ser escrito.

Para estar más cerca del ejemplo de su pregunta, puede escribirlo como whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(no importa si nombra el parámetro ko key). O escribir como whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);si tryToLetOutes statico whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);si tryToLetOutes un método de instancia.

Holger
fuente
114

Recientemente también estuve jugando con este método. Escribí un algoritmo memorizado para calcular los números de Fibonacci que podría servir como otra ilustración sobre cómo usar el método.

Podemos comenzar definiendo un mapa y poniendo los valores en él para los casos base, a saber, fibonnaci(0)y fibonacci(1):

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

Y para el paso inductivo, todo lo que tenemos que hacer es redefinir nuestra función de Fibonacci de la siguiente manera:

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

Como puede ver, el método computeIfAbsentutilizará la expresión lambda proporcionada para calcular el número de Fibonacci cuando el número no está presente en el mapa. Esto representa una mejora significativa con respecto al algoritmo tradicional recursivo de árbol.

Edwin Dalorzo
fuente
18
Agradable conversión de una sola línea a programación dinámica. Muy resbaladizo.
Benjamin H
3
¿Puede recibir menos llamadas recursivas si tiene la llamada (n-2) primero?
Thorbjørn Ravn Andersen
9
Debe tener más cuidado cuando use computeIfAbsent de forma recursiva. Para obtener más detalles, consulte stackoverflow.com/questions/28840047/…
Ajit Kumar
11
Este código da como resultado que HashMapse corrompan los componentes internos, al igual que en bugs.openjdk.java.net/browse/JDK-8172951 y fallará ConcurrentModificationExceptionen Java 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
22
Los documentos dicen literalmente que la función de mapeo no debería modificar este mapa durante el cálculo , por lo que esta respuesta es claramente incorrecta.
fps
41

Otro ejemplo. Al construir un mapa complejo de mapas, el método computeIfAbsent () es un reemplazo del método get () de map. Mediante el encadenamiento de llamadas computeIfAbsent () juntas, los contenedores faltantes se construyen sobre la marcha mediante las expresiones lambda proporcionadas:

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

multi-mapa

Esto es realmente útil si desea crear un multimapa sin recurrir a Google Guava biblioteca de para su implementación de MultiMap.

Por ejemplo, suponga que desea almacenar una lista de estudiantes que se matricularon en una materia en particular.

La solución normal para esto usando la biblioteca JDK es:

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

//continue....

Dado que tiene un código repetitivo, la gente tiende a usar Guava Mutltimap .

Usando Map.computeIfAbsent, podemos escribir en una sola línea sin guava Multimap de la siguiente manera.

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

Stuart Marks y Brian Goetz dieron una buena charla sobre esto https://www.youtube.com/watch?v=9uTVXxJjuco

nantitv
fuente
Otra forma de hacer un multimapa en Java 8 (y más conciso) es simplemente hacerlo. studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());Esto produce un multimapa de tipo Map<T,List<T>en JDK solo que de manera más concisa en mi humilde opinión.
Zombies