Wie kann ich vermeiden, dass Code wiederholt wird, der eine Hashmap von Hashmap initialisiert?

27

Jeder Kunde hat eine ID und viele Rechnungen mit Datumsangaben, die als Hashmap von Kunden nach ID gespeichert sind, sowie eine Hashmap von Rechnungen nach Datum:

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

Java-Lösung scheint zu verwenden getOrDefault:

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

Aber wenn get nicht null ist, möchte ich immer noch put (Datum, Rechnung) ausführen, und das Hinzufügen von Daten zu "allInvoicesAllClients" ist weiterhin erforderlich. Es scheint also nicht viel zu helfen.

Hernán Eche
quelle
Wenn Sie die Eindeutigkeit des Schlüssels nicht garantieren können, ist es am besten, wenn die sekundäre Karte den Wert "Liste <Rechnung>" anstelle von "Rechnung" hat.
Ryan

Antworten:

39

Dies ist ein ausgezeichneter Anwendungsfall für Map#computeIfAbsent. Ihr Snippet entspricht im Wesentlichen:

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

Wenn ides nicht als Schlüssel vorhanden ist allInvoicesAllClients, wird eine Zuordnung von idzu einer neuen erstellt HashMapund die neue zurückgegeben HashMap. Wenn ides als Schlüssel vorhanden ist, wird der vorhandene zurückgegeben HashMap.

Jacob G.
quelle
1
computeIfAbsent führt ein get (id) (oder einen Put, gefolgt von einem get (id)) aus, sodass der nächste put ausgeführt wird, um den put (date) des richtigen Elements zu korrigieren.
Hernán Eche
allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Alexander - Monica
1
@ Alexander-ReinstateMonica Map.oferstellt eine nicht veränderbare Map, die ich nicht sicher bin, ob das OP will.
Jacob G.
Wäre dieser Code weniger effizient als das, was das OP ursprünglich hatte? Fragen Sie dies, weil ich nicht weiß, wie Java mit Lambda-Funktionen umgeht.
Zecong Hu
16

computeIfAbsentist eine großartige Lösung für diesen speziellen Fall. Im Allgemeinen möchte ich Folgendes beachten, da es noch niemand erwähnt hat:

Die "äußere" Hashmap speichert nur einen Verweis auf die "innere" Hashmap, sodass Sie die Operationen einfach neu anordnen können, um die Codeduplizierung zu vermeiden:

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
quelle
So haben wir das jahrzehntelang gemacht, bevor Java 8 seine ausgefallene computeIfAbsent()Methode entwickelte!
Neil Bartlett
1
Ich verwende diesen Ansatz heute noch in Sprachen, in denen die Kartenimplementierung keine einzige Methode zum Abrufen oder Zurückgeben und Zurückgeben bei Abwesenheit bietet. Dass dies immer noch die beste Lösung in anderen Sprachen sein kann, kann erwähnenswert sein, obwohl diese Frage speziell für Java 8 markiert ist.
Quinn Mortimer
11

Sie sollten so gut wie nie die "Double Brace" -Karteninitialisierung verwenden.

{{  put(date, invoice); }}

In diesem Fall sollten Sie verwenden computeIfAbsent

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

Wenn für diese ID keine Karte vorhanden ist, fügen Sie eine ein. Das Ergebnis ist die vorhandene oder berechnete Karte. Sie können dann putElemente in dieser Karte mit der Garantie, dass sie nicht null sind.

Michael
quelle
1
Ich weiß nicht, wer abstimmt, nicht ich, vielleicht verwirrt einzeiliger Code allInvoicesAllClients, weil Sie ID anstelle von Datum verwenden, ich werde es bearbeiten
Hernán Eche
1
@ HernánEche Ah. Mein Fehler. Vielen Dank. Ja, das Put-for idist auch erledigt. Sie können sich computeIfAbsenteinen bedingten Put vorstellen, wenn Sie möchten. Und es gibt auch den Wert zurück
Michael
" Sie sollten so gut wie nie die" Double Brace "-Karteninitialisierung verwenden. " Warum? (Ich bezweifle nicht, dass Sie Recht haben; ich
Heinzi
1
@Heinzi Weil es eine anonyme innere Klasse schafft. Dies enthält einen Verweis auf die Klasse, die sie deklariert hat. Wenn Sie die Karte verfügbar machen (z. B. über einen Getter), wird verhindert, dass die einschließende Klasse durch Müll gesammelt wird. Ich finde es auch verwirrend für Leute, die mit Java weniger vertraut sind. Initialisierungsblöcke werden kaum verwendet, und wenn man sie so schreibt, sieht es so aus, als hätte es {{ }}eine besondere Bedeutung, die es nicht hat.
Michael
1
@ Michael: Sinnvoll, danke. Ich habe völlig vergessen, dass anonyme innere Klassen immer nicht statisch sind (auch wenn sie es nicht müssen).
Heinzi
5

Dies ist länger als die anderen Antworten, aber imho weitaus besser lesbar:

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

allInvoicesAllClients.get(id).put(date, invoice);
Wolf
quelle
3
Dies mag für eine HashMap funktionieren, aber der allgemeine Ansatz ist nicht optimal. Wenn dies ConcurrentHashMaps waren, sind diese Operationen nicht atomar. In diesem Fall führt Check-Then-Act zu Rennbedingungen. Trotzdem positiv bewertet, für die Hasser.
Michael
0

Sie machen hier zwei verschiedene Dinge: Sicherstellen, dass die HashMap vorhanden ist, und Hinzufügen des neuen Eintrags.

Der vorhandene Code stellt sicher, dass das neue Element zuerst eingefügt wird, bevor die Hash-Map registriert wird. Dies ist jedoch nicht erforderlich, da die HashMap die Reihenfolge hier nicht wichtig ist. Keine der beiden Varianten ist threadsicher, sodass Sie nichts verlieren.

Wie @Heinzi vorgeschlagen hat, können Sie diese beiden Schritte einfach aufteilen.

Was ich auch tun würde, ist die Erstellung des HashMapin das allInvoicesAllClientsObjekt zu verlagern, so dass die getMethode nicht zurückkehren kann null.

Dies verringert auch die Möglichkeit für Rennen zwischen separaten Threads, von denen beide nullZeiger abrufen getund sich dann für puteinen neuen HashMapmit einem einzelnen Eintrag entscheiden könnten - der zweite putwürde wahrscheinlich den ersten verwerfen und das InvoiceObjekt verlieren .

Simon Richter
quelle