简体   繁体   中英

How can I avoid repeating code initializing a hashmap of hashmap?

Every client has an id, and many invoices, with dates, stored as Hashmap of clients by id, of a hashmap of invoices by 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);
}

Java solution seems to be to use getOrDefault :

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

But if get is not null, I still want put (date, invoice) to execute, and also adding data to "allInvoicesAllClients" is still needed. So it doesn't seem to help much.

This is an excellent use-case for Map#computeIfAbsent . Your snippet is essentially equivalent to:

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

If id isn't present as a key in allInvoicesAllClients , then it'll create mapping from id to a new HashMap and return the new HashMap . If id is present as a key, then it'll return the existing HashMap .

computeIfAbsent is a great solution for this particular case. In general, I'd like to note the following, since nobody mentioned it yet:

The "outer" hashmap just stores a reference to the "inner" hashmap, so you can just reorder the operations to avoid the code duplication:

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

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

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

You should pretty much never use "double brace" map initialization.

{{  put(date, invoice); }}

In this case, you should use computeIfAbsent

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

If there is no map for this ID, you will insert one. The result will be the existing or computed map. You can then put items in that map with guarantee that it won't be null.

This is longer than the other answers, but imho far more readable:

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

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

You are doing two separate things here: ensuring that the HashMap exists, and adding the new entry to it.

The existing code makes sure to insert the new element first before registering the hash map, but that isn't necessary, because the HashMap doesn't care about the ordering here. Neither variant is threadsafe, so you're not losing anything.

So, like @Heinzi suggested, you can just split these two steps.

What I would also do is offload the creation of the HashMap to the allInvoicesAllClients object, so the get method cannot return null .

This also reduces the possibility for races between separate threads that could both get null pointers from get and then decide to put a new HashMap with a single entry -- the second put would probably discard the first, losing the Invoice object.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM