Comment puis-je éviter de répéter le code initialisant un hashmap de hashmap?

27

Chaque client a un identifiant, et de nombreuses factures, avec des dates, stockées sous forme de Hashmap de clients par identifiant, d'un hashmap de factures par date:

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 solution Java semble être d'utiliser getOrDefault:

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

Mais si get n'est pas nul, je veux toujours que put (date, facture) s'exécute, et l'ajout de données à "allInvoicesAllClients" est toujours nécessaire. Cela ne semble donc pas beaucoup aider.

Hernán Eche
la source
Si vous ne pouvez pas garantir l'unicité de la clé, le mieux est de faire en sorte que la carte secondaire ait la valeur List <Invoice> au lieu de simplement Invoice.
Ryan

Réponses:

39

C'est un excellent cas d'utilisation pour Map#computeIfAbsent. Votre extrait est essentiellement équivalent à:

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

S'il idn'est pas présent en tant que clé allInvoicesAllClients, il créera un mappage de idvers un nouveau HashMapet retournera le nouveau HashMap. Si idest présent sous forme de clé, il renverra l'existant HashMap.

Jacob G.
la source
1
computeIfAbsent, fait un get (id) (ou un put suivi d'un get (id)), donc le prochain put est fait pour corriger l'élément put (date), bonne réponse.
Hernán Eche
allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Alexander - Rétablir Monica
1
@ Alexander-ReinstateMonica Map.ofcrée un élément non modifiable Map, dont je ne suis pas sûr que l'OP le souhaite.
Jacob G.20
Ce code serait-il moins efficace que ce que l'OP avait à l'origine? Poser cette question parce que je ne sais pas comment Java gère les fonctions lambda.
Zecong Hu
16

computeIfAbsentest une excellente solution pour ce cas particulier. En général, je voudrais noter ce qui suit, car personne ne l'a encore mentionné:

La table de hachage "externe" stocke simplement une référence à la table de hachage "intérieure", vous pouvez donc simplement réorganiser les opérations pour éviter la duplication de code:

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
la source
C'est ainsi que nous avons fait cela pendant des décennies avant que Java 8 ne propose sa computeIfAbsent()méthode sophistiquée !
Neil Bartlett
1
J'utilise toujours cette approche aujourd'hui dans des langues où l'implémentation de la carte ne fournit pas une seule méthode get-or-put-and-return-if-absent. Que cela puisse encore être la meilleure solution dans d'autres langues peut être utile de mentionner même si cette question est spécifiquement balisée pour Java 8.
Quinn Mortimer
11

Vous ne devriez pratiquement jamais utiliser l'initialisation de carte "double accolade".

{{  put(date, invoice); }}

Dans ce cas, vous devez utiliser computeIfAbsent

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

S'il n'y a pas de carte pour cet ID, vous en insérerez une. Le résultat sera la carte existante ou calculée. Vous pouvez ensuite putajouter des éléments dans cette carte avec la garantie qu'elle ne sera pas nulle.

Michael
la source
1
Je ne sais pas qui dévalorise, pas moi, peut-être que le code à une seule ligne déroute toutInInvoicesAllClients, car vous utilisez id au lieu de date, je vais le modifier
Hernán Eche
1
@ HernánEche Ah. Mon erreur. Merci. Oui, le put idest également fait. Vous pouvez penser computeIfAbsentà un put conditionnel si vous le souhaitez. Et il renvoie également la valeur
Michael
" Vous ne devriez pratiquement jamais utiliser l'initialisation de carte" double accolade ". " Pourquoi? (Je ne doute pas que vous ayez raison; je demande par
pure
1
@Heinzi Parce qu'il crée une classe interne anonyme. Cela contient une référence à la classe qui l'a déclarée, ce qui si vous exposez la carte (par exemple via un getter) empêchera la classe englobante d'être récupérée. De plus, je trouve que cela peut être déroutant pour les personnes moins familiarisées avec Java; les blocs d'initialisation ne sont presque jamais utilisés, et l'écrire ainsi donne {{ }}une signification particulière, ce qui n'est pas le cas.
Michael
1
@Michael: C'est logique, merci. J'ai totalement oublié que les classes internes anonymes sont toujours non statiques (même si elles n'ont pas besoin de l'être).
Heinzi
5

C'est plus long que les autres réponses, mais à mon humble avis beaucoup plus lisible:

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

allInvoicesAllClients.get(id).put(date, invoice);
Loup
la source
3
Cela peut fonctionner pour un HashMap mais l'approche générale n'est pas optimale. S'il s'agissait de ConcurrentHashMaps, ces opérations ne sont pas atomiques. Dans ce cas, check-then-act entraînera des conditions de course. Toujours voté, f les ennemis.
Michael
0

Vous faites deux choses distinctes ici: vous assurer que le HashMapexiste et y ajouter la nouvelle entrée.

Le code existant s'assure d'insérer le nouvel élément avant d'enregistrer la carte de hachage, mais ce n'est pas nécessaire, car le HashMapne se soucie pas de l'ordre ici. Aucune des deux variantes n'est threadsafe, donc vous ne perdez rien.

Ainsi, comme l'a suggéré @Heinzi, vous pouvez simplement diviser ces deux étapes.

Ce que je ferais aussi, c'est décharger la création de HashMapl' allInvoicesAllClientsobjet vers l' objet, donc la getméthode ne peut pas revenir null.

Cela réduit également la possibilité de courses entre des threads séparés qui pourraient à la fois obtenir des nullpointeurs getet ensuite décider d' putune nouvelle HashMapavec une seule entrée - la seconde putrejetterait probablement la première, perdant l' Invoiceobjet.

Simon Richter
la source