【问题标题】:How does HashSet not allow duplicates?HashSet 如何不允许重复?
【发布时间】:2014-04-08 20:45:37
【问题描述】:

我正在通过HashSet 的add 方法。提到了

如果该集合已经包含该元素,则调用保持该集合不变并返回 false。

但add 方法在内部保存HashMap 中的值

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

HashMap 的 put 方法声明

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

那么如果HashMap 的put 方法替换了旧值,那么HashSet add 方法如何在重复元素的情况下保持集合不变?

【问题讨论】:

  • 感谢您查找 HashSet.add 的源代码。你查过HashMap.put的来源吗?

标签: java hashmap hashset


【解决方案1】:

您可能正在寻找的答案归结为支持哈希映射将集合的元素映射到值PRESENT,该值在HashSet.java 中定义如下:

private static final Object PRESENT = new Object();

在HashMap.put 的源代码中我们有:

  386     public V put(K key, V value) {
  387         if (key == null)
  388             return putForNullKey(value);
  389         int hash = hash(key.hashCode());
  390         int i = indexFor(hash, table.length);
  391         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  392             Object k;
  393             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  394                 V oldValue = e.value;
  395                 e.value = value;
  396                 e.recordAccess(this);
  397                 return oldValue;
  398             }
  399         }
  400 
  401         modCount++;
  402         addEntry(hash, key, value, i);
  403         return null;
  404     }

因为有问题的键已经存在,所以我们将在第 397 行提前返回。但您可能会认为第 395 行正在对映射进行更改,其中似乎我们正在更改映射的值入口。但是,value 的值是PRESENT。但是因为PRESENT是静态的和最终的,所以只有一个这样的实例;所以分配e.value = value 实际上并没有改变地图,因此根本没有改变集合!

更新:

一旦HashSet 被初始化。
- 其中的所有项目都作为键存储在HashMap
- 所有HashMap 的值只有一个对象PRESENT,这是HashSet 中的一个静态字段

【讨论】:

  • @RayToal,你是如何复制代码和行号的??
  • 复制自here
【解决方案2】:

来自 HashMap.put() 的 javadocs, "将指定值与此映射中的指定键相关联。如果映射先前包含该键的映射,则替换旧值。"

因此映射值将被替换,(这是HashSet类中的一个常量静态字段,因此相同的实例被替换),并且映射键保持不变(实际上是Set集合项)

【讨论】:

    【解决方案3】:

    PRESENT 只是一个虚拟值——集合并不真正关心它是什么。集合 关心的是地图的键。所以逻辑是这样的:

    Set.add(a):
      map.put(a, PRESENT) // so far, this is just what you said
        the key "a" is in the map, so...
          keep the "a" key, but map its value to the PRESENT we just passed in
          also, return the old value (which we'll call OLD)
      look at the return value: it's OLD, != null. So return false.
    

    现在,OLD == PRESENT 并不重要——请注意 Map.put 不会更改键,只是映射到该键的值。由于地图的键是Set真正关心的,所以Set没有改变。

    事实上,Set 的底层结构发生了一些变化——它用(a, PRESENT) 替换了(a, OLD) 的映射。但这在Set 的实现之外是无法观察到的。 (事实上​​,这种变化甚至不是真正的变化,因为OLD == PRESENT)。

    【讨论】:

    • 很好的解释。谢谢:)
    【解决方案4】:
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    

    e 是关键,所以如果 e 已经存在 put 将不会返回 null。因此add 将返回 false。

    put 的 JavaDoc:

    与 key 关联的先前值,如果没有 key 映射,则为 null。 (返回 null 也可以表明映射之前将 null 与 key 关联。)

    【讨论】:

      【解决方案5】:

      见HashMap#put:

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

      它将键替换为 new 值,这样HashSet 中就不会出现重复项。

      【讨论】:

      • 我知道HashSet 中没有重复值,我想知道为什么旧的重复值没有在HashSet 中被覆盖?
      • @Shaan, map 值可以被替换,(它是 HashSet 类中的一个常量静态字段,因此被相同的实例替换),并且 map 键保持不变(实际上是Set 集合项)
      【解决方案6】:

      如您所见,HashSet.add 方法将元素作为键而不是值添加到 HashMap.put。 HashMap 中的值被替换,而不是键。

      【讨论】:

      • 是的。地图中的所有值都相同(PRESENT)。 HashSet 使用内部映射及其 KEY 部分来存储 Set 的元素
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-03
      • 2015-09-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多