如何避免重复代码初始化hashmap的hashmap?


27

每个客户都有一个ID,以及许多带有日期的发票,这些日期按ID存储为客户的Hashmap,即按日期的发票哈希图:

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解决方案似乎是要使用的getOrDefault

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

但是,如果get不为null,我仍然希望执行put(日期,发票),并且仍然需要将数据添加到“ allInvoicesAllClients”。因此,它似乎并没有太大帮助。


如果不能保证键的唯一性,那么最好是让辅助映射具有List <Invoice>的值,而不只是Invoice。
瑞安

Answers:


39

这是一个很好的用例Map#computeIfAbsent。您的代码段基本上等于:

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

如果id没有作为键输入allInvoicesAllClients,则它将创建从id到新映射的映射HashMap并返回新映射HashMap。如果id存在作为密钥,则它将返回现有的HashMap


1
computeIfAbsent,执行get(id)(或由get(id)跟随的看跌期权),因此完成下一个看跌期权以更正项目put(date),正确答案。
HernánEche

allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
亚历山大-莫妮卡

1
@ Alexander-ReinstateMonica Map.of创建一个unmodifiable Map,我不确定OP是否想要。
雅各布·G

该代码的效率是否会低于OP最初的效率?提出这个问题是因为我不熟悉Java如何处理lambda函数。
胡泽聪

16

computeIfAbsent对于这种特殊情况是一个很好的解决方案。总的来说,我想指出以下几点,因为还没有人提及:

“外部”哈希表仅存储对“内部”哈希表的引用,因此您可以对操作进行重新排序以避免代码重复:

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

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

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

这是我们在Java 8出现其花哨的computeIfAbsent()方法之前数十年来所做的事情!
Neil Bartlett

1
今天,我仍然在地图实现未提供单一的缺少时获取或返回的语言中使用这种方法。这还可以在其他语言中最好的解决方案可能是值得一提,即使这个问题是特别标记为Java 8
奎因莫蒂默

11

您应该几乎永远不要使用“双括号”映射初始化。

{{  put(date, invoice); }}

在这种情况下,您应该使用 computeIfAbsent

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

如果此ID没有映射,则将插入一个。结果将是现有的或计算的地图。然后put,可以确保该映射中的项目不会为空。


1
我不知道是谁downvote,不是我,也许一行代码混淆allInvoicesAllClients说,因为你使用的ID,而不是日期,我将修改它
埃尔南Eche

1
@HernánEche啊。我的错。谢谢。是的,认沽权也id已完成。computeIfAbsent如果愿意,您可以将其视为有条件看跌期权。它也返回值
Michael

您应该几乎永远不要使用“双括号”映射初始化。 ”为什么?(我毫不怀疑您说的对;我出于好奇而问。)
Heinzi

1
@Heinzi因为它创建了一个匿名内部类。这保留了对声明了它的类的引用,如果您公开了地图(例如,通过getter),则将防止封闭的类被垃圾回收。另外,我发现对于不太熟悉Java的人可能会感到困惑。很少使用初始化程序块,并且像这样编写它使它看起来{{ }}具有特殊的含义,而没有。
Michael

1
@Michael:很好,谢谢。我完全忘记了匿名内部类始终是非静态的(即使不需要)。
Heinzi

5

这比其他答案更长,但是恕我直言,可读性更高:

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

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

3
这可能适用于HashMap,但一般方法不是最佳方法。如果这些是ConcurrentHashMaps,则这些操作不是原子的。在这种情况下,先检查后行动将导致比赛状况。无论如何,仇恨者都支持。
Michael

0

您在这里做两件事:确保HashMap存在,并向其中添加新条目。

现有代码可确保在注册哈希映射之前先插入新元素,但这不是必需的,因为这HashMap并不关心此处的顺序。两种变体都不是线程安全的,因此您不会丢失任何东西。

因此,就像@Heinzi建议的那样,您可以将这两个步骤分开。

我还要做的是将的创建工作分担HashMap给该allInvoicesAllClients对象,因此该get方法无法返回null

这也减少了单独的线程可能都得到之间的比赛的可能性null从三分球get,然后决定put一个新的HashMap一次入境-第二个put可能会放弃第一,失去了Invoice对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.