【问题标题】:Mutation of the keys in HashMap causes wrong resultsHashMap 中键的突变导致错误的结果
【发布时间】:2017-08-31 08:54:22
【问题描述】:

在我的项目中,我使用 HashMap 来存储一些数据,最近我发现当我改变 HashMap 的键时,可能会出现一些意想不到的错误结果。例如:

HashMap<ArrayList,Integer> a = new HashMap<>();
ArrayList list1 = new ArrayList<>();
a.put(list1, 1);
System.out.println(a.containsKey(new ArrayList<>())); // true
list1.add(5);
ArrayList list2 = new ArrayList<>();
list2.add(5);
System.out.println(a.containsKey(list2)); // false

注意a.keySet().iterator().next().hashCode() == list2.hashCode()a.keySet().iterator().next().equals(list2) 都是真的。

我不明白为什么会这样,指的是两个对象相等并且具有相同的哈希码这一事实。有谁知道这是什么原因,以及是否有任何其他类似的结构允许密钥的突变?谢谢。

【问题讨论】:

  • 不要使用可变值作为 hashmap 键。我以为它在 HashMap 的文档中某处说,但我现在找不到它。
  • 我知道我不应该(关于我展示的示例)但我无法理解原因(对象相等并且具有相同的哈希码)并且我正在寻找类似的结构这允许。
  • "...是否有任何其他类似的结构允许键的突变?" - 密钥(至少构成密钥的部分)永远不会是可变的,因为这会妨碍它们的使用。把它想象成一把现实世界的钥匙:如果你改变你的钥匙,它就不会再打开锁了。如果你换了锁,你需要一把新钥匙。

标签: java hashmap


【解决方案1】:

可变键总是一个问题。如果突变可以改变它们的哈希码和/或equals() 的结果,则认为键是可变的。话虽如此,列表通常会生成其哈希码并根据其元素检查相等性,因此它们几乎不是映射键的良好候选者。

您的示例中有什么问题?添加键时,它是一个空列表,因此产生的哈希码与包含元素时不同。因此,即使键的哈希码和list2 相同 更改键列表后,您也不会找到该元素。为什么?仅仅是因为地图在错误的存储桶中查找。

示例(简化):

让我们从几个假设开始:

  • 空列表返回哈希码 0
  • 如果列表包含元素 5,则返回哈希码 5
  • 我们的地图有 16 个桶(默认)
  • 桶索引由哈希码 % 16(我们的桶数)确定

如果现在添加空列表,由于其哈希码,它会被插入到存储桶 0 中。

当您使用 list1 进行查找时,由于哈希码为 5,它将在存储桶 5 中查找。由于该存储桶为空,因此将找不到任何内容。

问题是您的密钥列表更改了其哈希码,因此应该放入不同的存储桶中,但地图不知道这应该发生(这样做可能会导致一堆其他问题)。

【讨论】:

    【解决方案2】:

    根据Map 的javadocs:

    注意:如果将可变对象用作映射,则必须非常小心 键。如果对象的值未指定映射的行为 以影响等于比较的方式进行更改,而 object 是地图中的一个键。该禁令的一个特例是 不允许地图包含自己作为键。虽然它 允许地图将自身包含为一个值,非常小心 建议:equals 和 hashCode 方法不再定义良好 在这样的地图上。

    您的列表是关键,您正在更改它们。 如果列表的内容不是确定哈希码的值和什么是相等的,这将不是问题,但是这不是你的情况。如果您考虑一下,更改地图的键没有多大意义。关键是识别价值的东西,如果该关键发生变化,所有赌注都将被取消。

    映射在插入时插入给定哈希码的值。当您稍后搜索它时,它会使用参数的哈希码来确定它是否命中。我想你会发现,如果你插入了 list1 和已经插入的值,你会看到打印出“true”,因为 list2.hashCode() 会在插入时产生与 list1 相同的哈希码。

    【讨论】:

      【解决方案3】:

      这是因为 HashMap 使用 Object 的 hashCode() 方法结合 equals(Object obj) 来检查此映射是否包含对象。

      见:

      ArrayList<Integer> a = new ArrayList<>();
      a.add(1);
      System.out.println(a.hashCode());
      a.add(2);
      System.out.println(a.hashCode());
      

      这个例子表明,你的 ArrayList 的 hashCode 已经改变了。

      【讨论】:

      • 啊,我明白了。谢谢。不知道有没有类似的数据结构允许key mutation?
      • @RanSch 也许IdentityHashMap 适合你?
      • @Thomas 不,因为我需要“等于”检查,但谢谢。
      【解决方案4】:

      你不应该在你的 hashmap 中使用可变对象作为键。

      因此,当您将 list1 作为第 3 行的键时,基本上发生的情况是地图计算其 hashCode,稍后将在 containsKey(someKey) 中进行比较。 但是当你在第 5 行改变 list1 时,它的 hashCode 基本上改变了。 所以如果你现在这样做

      System.out.println(a.containsKey(list1)); 
      

      在第 5 行之后会显示 false 如果你这样做System.out.println(a.get(list1));

      它会说null,因为它比较了两个不同的哈希码

      【讨论】:

        【解决方案5】:

        可能你没有覆盖 equals() 和 hashCode() 方法。

        【讨论】:

        • 他们已经实现了。
        猜你喜欢
        • 2021-06-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-05
        • 1970-01-01
        • 2014-06-27
        • 2020-03-19
        • 2020-02-18
        相关资源
        最近更新 更多