【问题标题】:Java's HashMap key replacement when storing existing value存储现有值时 Java 的 HashMap 键替换
【发布时间】:2020-12-03 18:07:55
【问题描述】:

根据 Java HashMap 文档,put 方法替换了之前包含的值(如果有):https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html#put-K-V-

将指定的值与此映射中的指定键相关联。如果 映射先前包含键的映射,旧值是 换了。

但是,文档并没有说明在存储新值时(现有)键会发生什么。现有密钥是否被替换?还是结果未定义?

考虑以下示例:

public class HashMapTest
{

   private static class Key {
      private String value;
      private Boolean b;

      private Key(String value, Boolean b) {
         this.value = value;
         this.b = b;
      }

      @Override
      public int hashCode()
      {
         return value.hashCode();
      }

      @Override
      public boolean equals(Object obj)
      {
         if (obj instanceof Key)
         {
            return value.equals(((Key)obj).value);
         }
         return false;
      }

      @Override
      public String toString()
      {
         return "(" + value.toString() + "-" + b + ")";
      }
   }

   public static void main(String[] arg) {

      Key key1 = new Key("foo", true);
      Key key2 = new Key("foo", false);

      HashMap<Key, Object> map = new HashMap<Key, Object>();
      map.put(key1, 1L);

      System.out.println("Print content of original map:");
      for (Entry<Key, Object> entry : map.entrySet()) {
         System.out.println("> " + entry.getKey() + " -> " + entry.getValue());
      }

      map.put(key2, 2L);

      System.out.println();
      System.out.println("Print content of updated map:");
      for (Entry<Key, Object> entry : map.entrySet()) {
         System.out.println("> " + entry.getKey() + " -> " + entry.getValue());
      }
   }
}

当我使用 Oracle jdk1.8.0_121 执行以下代码时,会产生以下输出:

Print content of original map:
> (foo-true) -> 1

Print content of updated map:
> (foo-true) -> 2

有证据表明(至少在我的 PC 上)现有密钥不会被替换。

这是预期/定义的行为(它在哪里定义?)还是只是所有可能结果中的一种?我可以指望这种行为在所有 Java 平台/版本中保持一致吗?

编辑:此问题与What happens when a duplicate key is put into a HashMap? 不重复。我问的是键(即当您使用多个键实例引用同一个逻辑键时),而不是值。

【问题讨论】:

  • “如果映射先前包含键的映射,则替换旧值。”所以值被替换,(foo-true) -&gt; 1 旧值是1,替换后(foo-true) -&gt; 2 新值是2
  • 是的,HashMap 使用 HashCode,根据 equals 和 hashcode 协定,检查键的唯一性。关键是您的 HashMap POV 中的 2 个键完全相同,因为您的 hashcode/equals 实现。所以不,没有更换钥匙,好地方!
  • 您好 Anton Balaniuc,感谢您的回答。不过,我问的是关键,而不是价值。我完全同意最终的值一定是2(因为key1key2这两个键指的是同一个逻辑键),但是我在问自己,因为第二个键key2没有替换第一个键@987654334 @ 在 HashMap 中。

标签: java hashmap


【解决方案1】:

source看,它没有被替换,我不确定它是否有合同保证。

if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

它找到现有的映射并替换值,新键不做任何事情,它们应该是相同且不可变的,因此即使不同的实现可以替换键也无关紧要。

您不能指望这种行为,但您应该以一种无关紧要的方式编写代码。

【讨论】:

  • 您好奥列格,谢谢您的回答!您的回答反映了我运行代码的结果,但没有说明合同的任何内容。我很想知道这是否在某处描述和/或在 Oracle 的专有 JCK/TCK 测试套件中进行了测试(用于确定 Java 实现是否是标准的)
【解决方案2】:

添加新对时,映射使用hasCode,equals 来检查键是否已存在于映射中。如果密钥已经存在,则旧值将替换为新值。密钥本身保持不变。

Map<Integer,String> map = new HashMap<>();
map.put(1,"two"); 
System.out.println(map); // {1=two}
map.put(1,"one"); 
System.out.println(map); // {1=one}
map.put(2,"two");
System.out.println(map); // {1=one, 2=two}

您的equalshashCode 合同存在问题。根据您的实现,ke1key2 是相同的:

@Override
public boolean equals(Object obj)
{
    if (obj instanceof Key)
    {
       return value.equals(((Key)obj).value);
    }
    return false;
}

你还需要比较Boolean b

Key other = (Key) obj;
return value.equals(other.value) && b.equals(other.b);

同样的规则适用于hasCode

@Override
public int hashCode()
{
    return value.hashCode();
}

return value.hashCode() + b.hashCode();

这些变化key1key2 是不同的

System.out.println(key1.equals(key2));

您的地图的输出将是

> (foo-true) -> 1
> (foo-false) -> 2

【讨论】:

  • 他是故意的。检查密钥是否被替换。
  • @Oleg,但是key不被替换,如果key重复,value被替换。
  • 是的,这就是我在回答中解释的内容,我查看了源代码进行验证,OP 决定用代码对其进行测试。
  • 您好 Anton Balaniuc,您在回答中写道“密钥本身保持未修改”。 (这是我在源代码中观察到的结果)。你为什么这么说?这是在哪里书面/记录的?
  • @Ciaccia docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V- 这里没有关于密钥修改的内容。如果密钥以任何方式被修改,它将反映在 java 文档中
【解决方案3】:

它没有被替换——它也不应该被替换。如果您知道HashMap 的工作原理以及hashCodeequals 是什么(或者更准确地说它们是如何使用的)- 不接触Key 的决定是显而易见的。

当您第二次将 other 键/条目放入映射时,该键首先在映射中查找 - 根据hashCode/equals,因此根据map IFF 键具有相同的 hashCode 并且根据equals 相等,它们是相同的。如果是这样,为什么要更换它?特别是因为如果它被替换,那可能会触发额外的操作或至少额外的代码如果键相等则不会触发任何其他东西。

【讨论】:

  • 您好尤金,谢谢您的回答。这是在哪里描述/记录的?您是否有权访问一些描述或预期这种行为的资源?
  • @Ciaccia 你在问哈希码/等于?你到底是什么意思
  • @Ciaccia 询问他最初询问的同一件事。显而易见的并不重要,如果合同不保证密钥不会被替换,它可以被替换。
  • @Oleg 因为代码是正确的,但不是;不过你确实有道理
【解决方案4】:

显然,当前的HashSet implementation 依赖于这种 HashMap 行为以符合 HashSet 文档。 我的意思是,当您在 HashSet 中添加一个新元素时,文档说如果您尝试在已经包含该元素的 HasSet 中添加一个元素,则 HashSet 不会更改,因此该元素不会被替换, 在 openjdk8 实现中,HashSet 使用 HashMap 键来保存值,在 HashSet.add 方法中它调用 HashMap.put 方法来添加值,因此依赖于 put 方法不会替换对象这一事实

虽然这仍然不是文档中的直接规范,而且它会随着 JRE 实现的变化而变化,但它可能会提供更强大的 保证这在未来可能不会改变

【讨论】:

    猜你喜欢
    • 2012-06-18
    • 1970-01-01
    • 1970-01-01
    • 2012-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-12
    相关资源
    最近更新 更多