Como evitar a repetição de código inicializando um hashmap de hashmap?

27

Todo cliente tem um ID e muitas faturas, com datas, armazenadas como Hashmap de clientes por ID, de um hashmap de faturas por data:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if(allInvoices!=null){
    allInvoices.put(date, invoice);      //<---REPEATED CODE
}else{
    allInvoices = new HashMap<>();
    allInvoices.put(date, invoice);      //<---REPEATED CODE
    allInvoicesAllClients.put(id, allInvoices);
}

A solução Java parece usar getOrDefault:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.getOrDefault(
    id,
    new HashMap<LocalDateTime, Invoice> (){{  put(date, invoice); }}
);

Mas se get não for nulo, eu ainda quero que a execução da data (fatura) seja executada, e também é necessário adicionar dados a "allInvoicesAllClients". Portanto, isso não parece ajudar muito.

Hernán Eche
fonte
Se você não pode garantir a exclusividade da chave, é melhor apostar que o mapa secundário tenha o valor de Lista <Fatura> em vez de apenas Fatura.
Ryan

Respostas:

39

Este é um excelente caso de uso para Map#computeIfAbsent. Seu snippet é essencialmente equivalente a:

allInvoicesAllClients.computeIfAbsent(id, key -> new HashMap<>()).put(date, invoice);

Se idnão estiver presente como chave allInvoicesAllClients, ele criará o mapeamento de idpara um novo HashMape retornará o novo HashMap. Se idestiver presente como chave, retornará o existente HashMap.

Jacob G.
fonte
11
computeIfAbsent, faz um get (id) (ou um put seguido por um get (id)), portanto, a próxima put é feita para corrigir o item put (date), a resposta certa.
Hernán Eche 19/03
allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Alexander - Restabelecer Monica
11
@ Alexander-ReinstateMonica Map.ofcria um modificável Map, que não tenho certeza se o OP deseja.
Jacob G.
Esse código seria menos eficiente do que o OP originalmente? Perguntando isso porque não estou familiarizado com o modo como o Java lida com as funções lambda.
Zecong Hu 26/03
16

computeIfAbsenté uma ótima solução para este caso em particular. Em geral, gostaria de observar o seguinte, já que ninguém o mencionou ainda:

O hashmap "externo" apenas armazena uma referência ao hashmap "interno", para que você possa reordenar as operações para evitar a duplicação de código:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if (allInvoices == null) {           
    allInvoices = new HashMap<>();
    allInvoicesAllClients.put(id, allInvoices);
}

allInvoices.put(date, invoice);      // <--- no longer repeated
Heinzi
fonte
Foi assim que fizemos isso por décadas antes do Java 8 aparecer com seu computeIfAbsent()método sofisticado !
Neil Bartlett
11
Ainda hoje uso essa abordagem em idiomas em que a implementação do mapa não fornece um único método de obter ou colocar e devolver se estiver ausente. Vale a pena mencionar que essa ainda pode ser a melhor solução em outras linguagens, mesmo que essa pergunta esteja especificamente marcada para Java 8.
Quinn Mortimer
11

Você nunca deve usar a inicialização de mapa com "chave dupla".

{{  put(date, invoice); }}

Nesse caso, você deve usar computeIfAbsent

allInvoicesAllClients.computeIfAbsent(id, (k) -> new HashMap<>())
                     .put(date, allInvoices);

Se não houver um mapa para esse ID, você inserirá um. O resultado será o mapa existente ou computado. Você pode então putitens nesse mapa com garantia de que não será nulo.

Michael
fonte
11
Eu não sei quem votou negativamente, não eu, talvez o código de linha única esteja confundindo todas as notas fiscais de todos os clientes, porque você usa o id em vez da data, eu o edito
Hernán Eche
11
@ HernánEche Ah. Meu erro. Obrigado. Sim, o put for também idestá feito. Você pode pensar computeIfAbsentem uma colocação condicional, se quiser. E também retorna o valor
Michael
" Você nunca deve usar a inicialização de mapa com" chave dupla ". " Por quê? (Não duvido que você esteja certo; estou perguntando por genuína curiosidade.)
Heinzi
11
@ Heinzi Porque cria uma classe interna anônima. Isso contém uma referência à classe que o declarou, que se você expuser o mapa (por exemplo, através de um getter) impedirá que a classe envolvente seja coletada como lixo. Além disso, acho que pode ser confuso para pessoas menos familiarizadas com Java; os blocos inicializadores quase nunca são usados, e escrevê-lo dessa maneira faz com que pareça {{ }}ter um significado especial, o que não acontece.
Michael
11
@ Michael: Faz sentido, obrigado. Esqueci totalmente que classes internas anônimas são sempre não estáticas (mesmo que não precisem ser).
Heinzi 25/03
5

Isso é mais longo que as outras respostas, mas é muito mais legível:

if(!allInvoicesAllClients.containsKey(id))
    allInvoicesAllClients.put(id, new HashMap<LocalDateTime, Invoice>());

allInvoicesAllClients.get(id).put(date, invoice);
Lobo
fonte
3
Isso pode funcionar para um HashMap, mas a abordagem geral não é ideal. Se estes eram ConcurrentHashMaps, essas operações não são atômicas. Nesse caso, a verificação do ato levará a condições de corrida. Votado de qualquer maneira, para os inimigos.
Michael
0

Você está fazendo duas coisas separadas aqui: garantir a existência HashMape adicionar a nova entrada a ela.

O código existente certifica-se de inserir o novo elemento primeiro antes de registrar o mapa de hash, mas isso não é necessário, porque HashMapele não se importa com o pedido aqui. Nenhuma das variantes é segura para threads, portanto você não está perdendo nada.

Então, como o @Heinzi sugeriu, você pode dividir essas duas etapas.

O que eu também faria é descarregar a criação de HashMappara o allInvoicesAllClientsobjeto, para que o getmétodo não possa retornar null.

Isso também reduz a possibilidade de corridas entre segmentos separados, que poderiam obter nullponteiros gete decidir putum novo HashMapcom uma única entrada - a segunda putprovavelmente descartaria a primeira, perdendo o Invoiceobjeto.

Simon Richter
fonte