Come posso evitare di ripetere il codice inizializzando una hashmap di hashmap?


27

Ogni cliente ha un ID e molte fatture, con date, memorizzate come hashmap dei clienti per ID, di una hashmap delle fatture per 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);
}

La soluzione Java sembra essere quella di utilizzare getOrDefault:

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

Ma se get non è nullo, voglio ancora che put (data, fattura) venga eseguito e che sia ancora necessario aggiungere dati a "allInvoicesAllClients". Quindi non sembra aiutare molto.


Se non riesci a garantire l'unicità della chiave, la soluzione migliore è che la mappa secondaria abbia il valore di Elenco <Invoice> anziché solo Invoice.
Ryan

Risposte:


39

Questo è un eccellente caso d'uso per Map#computeIfAbsent. Lo snippet equivale essenzialmente a:

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

Se idnon è presente come chiave allInvoicesAllClients, creerà la mappatura da iduna nuova HashMape restituirà la nuova HashMap. Se idè presente come chiave, restituirà l'esistente HashMap.


1
computeIfAbsent, esegue un get (id) (o un put seguito da un get (id)), quindi il put successivo viene eseguito per correggere l'elemento put (data), risposta corretta.
Hernán Eche,

allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Alexander - Ripristina Monica il

1
@ Alexander-ReinstateMonica Map.ofcrea un non modificabile Map, che non sono sicuro che l'OP vuole.
Jacob G.

Questo codice sarebbe meno efficiente di quello che l'OP aveva originariamente? Chiedere questo perché non ho familiarità con il modo in cui Java gestisce le funzioni lambda.
Zecong Hu,

16

computeIfAbsentè un'ottima soluzione per questo caso particolare. In generale, vorrei notare quanto segue, poiché nessuno lo ha ancora menzionato:

L'hashmap "esterna" memorizza solo un riferimento all'hashmap "interna", quindi puoi semplicemente riordinare le operazioni per evitare la duplicazione del codice:

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

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

allInvoices.put(date, invoice);      // <--- no longer repeated

È così che lo abbiamo fatto per decenni prima che Java 8 arrivasse con il suo computeIfAbsent()metodo elegante !
Neil Bartlett,

1
Uso ancora questo approccio oggi in lingue in cui l'implementazione della mappa non fornisce un singolo metodo get-or-put-and-return-if-assente. Che questa possa ancora essere la migliore soluzione in altre lingue può essere utile menzionare anche se questa domanda è specificamente taggata per Java 8.
Quinn Mortimer

11

Praticamente non dovresti mai usare l'inizializzazione della mappa "doppio controvento".

{{  put(date, invoice); }}

In questo caso, dovresti usare computeIfAbsent

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

Se non esiste una mappa per questo ID, ne inserirai una. Il risultato sarà la mappa esistente o calcolata. È quindi possibile putelementi in quella mappa con la garanzia che non sarà nulla.


1
Non so chi sia il downvote, non io, forse il codice a riga singola confonde tuttiInvoicesAllClients, perché usi id invece di data, lo modificherò
Hernán Eche

1
@ HernánEche Ah. Errore mio. Grazie. Sì, anche il gioco idè fatto. computeIfAbsentSe lo desideri, puoi pensare a una condizione condizionale. E restituisce anche il valore
Michael

" Praticamente non dovresti mai usare l'inizializzazione della mappa" doppio controvento ". " Perché? (Non dubito che tu abbia ragione; te lo chiedo per vera curiosità.)
Heinzi il

1
@Heinzi Perché crea una classe interna anonima. Questo contiene un riferimento alla classe che l'ha dichiarata, che se si espone la mappa (ad es. Tramite un getter) si eviterà che la classe acclusa venga raccolta in modo inutile. Inoltre, trovo che possa essere fonte di confusione per le persone che non hanno familiarità con Java; i blocchi di inizializzatore non vengono quasi mai utilizzati e scriverlo in questo modo sembra {{ }}avere un significato speciale, cosa che non accade.
Michael

1
@Michael: ha senso, grazie. Ho completamente dimenticato che le classi interne anonime sono sempre non statiche (anche se non hanno bisogno di esserlo).
Heinzi,

5

Questo è più lungo delle altre risposte, ma molto più leggibile:

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

allInvoicesAllClients.get(id).put(date, invoice);

3
Questo potrebbe funzionare per una HashMap ma l'approccio generale non è ottimale. Se si trattava di ConcurrentHashMaps, queste operazioni non sono atomiche. In tal caso, il check-then-act porterà a condizioni di gara. Ho comunque votato per gli odiatori.
Michael

0

Qui stai facendo due cose separate: assicurarti che HashMapesista e aggiungere la nuova voce ad essa.

Il codice esistente si assicura di inserire il nuovo elemento prima di registrare la mappa hash, ma questo non è necessario, perché qui HashMapnon importa l'ordine. Nessuna delle varianti è thread-safe, quindi non stai perdendo nulla.

Quindi, come suggerito da @Heinzi, puoi semplicemente dividere questi due passaggi.

Quello che vorrei fare è anche offload la creazione del HashMapal allInvoicesAllClientsoggetto, in modo che il getmetodo non può tornare null.

Ciò riduce anche la possibilità di gare tra thread separati che potrebbero entrambi ottenere nullpuntatori da gete quindi decidere di putun nuovo HashMapcon una singola voce: il secondo putprobabilmente eliminerebbe il primo, perdendo l' Invoiceoggetto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.