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

Posted

技术标签:

【中文标题】如何避免重复代码初始化 hashmap 的 hashmap?【英文标题】:How can I avoid repeating code initializing a hashmap of hashmap? 【发布时间】:2020-06-30 22:51:58 【问题描述】:

每个客户都有一个 id 和许多带有日期的发票,按 id 存储为客户的哈希图,按日期的发票哈希图:

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 (date, invoice) 执行,并且仍然需要向“allInvoicesAllClients”添加数据。所以它似乎没有多大帮助。

【问题讨论】:

如果你不能保证键的唯一性,你最好的办法是让二级映射的值是 List 而不是 Invoice。 【参考方案1】:

这是Map#computeIfAbsent 的绝佳用例。你的 sn-p 基本上相当于:

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

如果id 不是allInvoicesAllClients 中的键,那么它将创建从id 到新HashMap 的映射并返回新的HashMap。如果id 作为键存在,那么它将返回现有的HashMap

【讨论】:

computeIfAbsent,做一个 get (id) (或一个 put 后​​跟一个 get (id) ),所以下一个 put 是为了更正项目 put(date),正确答案。 allInvoicesAllClients.computeIfAbsent(id, key -&gt; Map.of(date, invoice)) @Alexander-ReinstateMonica Map.of 创建一个不可修改的Map,我不确定 OP 是否想要。 这段代码会不会比 OP 原来的效率低?问这个是因为我不熟悉 Java 如何处理 lambda 函数。【参考方案2】:

computeIfAbsent 是这种特殊情况的绝佳解决方案。总的来说,我想注意以下几点,因为还没有人提到它:

“外部”hashmap 只是存储了对“内部”hashmap 的引用,因此您只需重新排序操作即可避免代码重复:

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() 方法之前几十年,我们就是这样做的! 我今天仍然在地图实现不提供单个 get-or-put-and-return-if-absent 方法的语言中使用这种方法。尽管这个问题是专门为 Java 8 标记的,但这仍然可能是其他语言的最佳解决方案。【参考方案3】:

您几乎不应该使用“双括号”地图初始化。

  put(date, invoice); 

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

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

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

【讨论】:

我不知道谁投了反对票,不是我,也许单行代码混淆了 allInvoicesAllClients,因为你使用 id 而不是 date,我会编辑它 @HernánEche 啊。我的错。谢谢。是的,id 的 put 也已完成。如果您愿意,您可以将computeIfAbsent 视为条件放置。它也返回值 "您几乎不应该使用“双括号”地图初始化。" 为什么? (我不怀疑你是对的;我是出于真正的好奇而问的。) @Heinzi 因为它创建了一个匿名内部类。这包含对声明它的类的引用,如果您公开地图(例如通过 getter),它将防止封闭类被垃圾收集。此外,我发现它可能会让不太熟悉 Java 的人感到困惑。初始化程序块几乎从未使用过,这样写会使它看起来像 具有特殊含义,但它没有。 @Michael:有道理,谢谢。我完全忘记了匿名内部类总是非静态的(即使它们不需要)。【参考方案4】:

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

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

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

【讨论】:

这可能适用于 HashMap,但一般方法不是最佳的。如果这些是 ConcurrentHashMaps,那么这些操作就不是原子的。在这种情况下,check-then-act 将导致竞争条件。无论如何,反对者们都投了赞成票。【参考方案5】:

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

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

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

我还要做的是将HashMap 的创建卸载到allInvoicesAllClients 对象,因此get 方法不能返回null

这也降低了单独线程之间竞争的可能性,这些线程既可以从get 获得null 指针,然后决定put 一个新的HashMap 与单个条目 - 第二个put 可能会丢弃第一个,丢失Invoice 对象。

【讨论】:

以上是关于如何避免重复代码初始化 hashmap 的 hashmap?的主要内容,如果未能解决你的问题,请参考以下文章

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

Rails 成语避免在 has_many 中重复:通过

使用聚合操作时如何避免在 data.table 中创建重复项

初始化日期时间值同时避免代码重复的最佳实践

如何避免与 Doctrine2 和 Zend Framework 2 的多对多关系重复?

C++预处理器:避免成员变量列表的代码重复