Como faço para usar a nova função computeIfAbsent?

115

Eu gostaria muito de usar Map.computeIfAbsent, mas já faz muito tempo que lambdas na graduação.

Quase diretamente dos documentos: dá um exemplo da velha maneira de fazer as coisas:

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 a nova forma:

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

Mas, no exemplo deles, acho que não estou "entendendo". Como eu transformaria o código para usar a nova forma lambda de expressar isso?

Benjamin H
fonte
Não tenho certeza do que você não entendeu com o exemplo aqui?
Louis Wasserman,
2
O que é "k"? É uma variável sendo definida? Que tal "novo valor" - é algo de java 8 ou representa um objeto que preciso definir ou substituir? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) não compila, então estou perdendo algo ...
Benjamin H
O que exatamente não compila? Que erro isso produz?
axtavt
Temp.java:26: erro: início ilegal da expressão whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (apontando para ">")
Benjamin H
Compila bem para mim. Certifique-se de realmente usar o compilador Java 8. Outros recursos do Java 8 funcionam?
axtavt

Respostas:

96

Suponha que você tenha o seguinte 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();
    }
}

Então você verá a mensagem creating a value for "snoop"exatamente uma vez, pois na segunda invocação de computeIfAbsentjá existe um valor para aquela chave. O kna expressão lambda k -> f(k)é apenas um placeolder (parâmetro) para a chave que o mapa passará para seu lambda para calcular o valor. Portanto, no exemplo, a chave é passada para a invocação da função.

Alternativamente, você pode escrever: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());para obter o mesmo resultado sem um método auxiliar (mas você não verá a saída de depuração então). E ainda mais simples, pois é uma delegação simples para um método existente que você pode escrever: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);Esta delegação não precisa de nenhum parâmetro para ser escrito.

Para ficar mais próximo do exemplo em sua pergunta, você pode escrever como whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(não importa se você nomeia o parâmetro kou key). Ou escreva como whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);se tryToLetOutfosse staticou whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);se tryToLetOutfosse um método de instância.

Holger
fonte
114

Recentemente, também comecei a usar esse método. Eu escrevi um algoritmo memoized para calcular os números de Fibonacci que podem servir como outra ilustração sobre como usar o método.

Podemos começar definindo um mapa e colocando os valores nele para os casos base, a saber, 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 para a etapa indutiva, tudo o que temos a fazer é redefinir nossa função Fibonacci da seguinte maneira:

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

Como você pode ver, o método computeIfAbsentusará a expressão lambda fornecida para calcular o número de Fibonacci quando o número não estiver presente no mapa. Isso representa uma melhoria significativa em relação ao algoritmo tradicional de árvore recursiva.

Edwin Dalorzo
fonte
18
Boa conversão de linha única para programação dinâmica. Muito habilidoso.
Benjamin H
3
Você pode obter menos chamadas recursivas se tiver a chamada (n-2) primeiro?
Thorbjørn Ravn Andersen
10
Você deve ter mais cuidado ao usar computeIfAbsent recursivamente. Para obter mais detalhes, consulte stackoverflow.com/questions/28840047/…
Ajit Kumar
12
Este código resulta na HashMapcorrupção dos internos de, assim como em bugs.openjdk.java.net/browse/JDK-8172951 e irá falhar com ConcurrentModificationExceptionno Java 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
23
Os documentos dizem literalmente que a função de mapeamento não deve modificar este mapa durante o cálculo , então essa resposta está claramente errada.
fps
41

Outro exemplo. Ao construir um mapa complexo de mapas, o método computeIfAbsent () substitui o método get () do mapa. Por meio do encadeamento de chamadas computeIfAbsent (), os contêineres ausentes são construídos instantaneamente pelas expressões lambda fornecidas:

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

multi-mapa

Isso é realmente útil se você deseja criar um multimapa sem recorrer à biblioteca Google Guava para a implementação deMultiMap .

Por exemplo, suponha que você queira armazenar uma lista de alunos que se inscreveram em um determinado assunto.

A solução normal para isso usando a biblioteca JDK é:

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

//continue....

Por ter algum código padrão, as pessoas tendem a usar Guava Mutltimap.

Usando Map.computeIfAbsent, podemos escrever em uma única linha sem Guava Multimap como segue.

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

Stuart Marks e Brian Goetz fizeram uma boa palestra sobre isso https://www.youtube.com/watch?v=9uTVXxJjuco

nantitv
fonte
Outra maneira de fazer um multimap em Java 8 (e mais conciso) é apenas fazer studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());Isso produz um multimapa do tipo Map<T,List<T>em JDK, mas de forma mais concisa imho.
Zombies