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?


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.


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.


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

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


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.