【问题标题】:Java HashMap resizingJava HashMap 调整大小
【发布时间】:2017-09-05 13:23:31
【问题描述】:

假设我们有一些代码

class WrongHashCode{
    public int code=0;

    @Override
    public int hashCode(){
        return code;
    }
}
public class Rehashing {
    public static void main(String[] args) {

        //Initial capacity is 2 and load factor 75%
        HashMap<WrongHashCode,String> hashMap=new HashMap<>(2,0.75f);

        WrongHashCode wrongHashCode=new WrongHashCode();
        //put object to be lost
        hashMap.put(wrongHashCode,"Test1");

        //Change hashcode of same Key object
        wrongHashCode.code++;

        //Resizing hashMap involved 'cause load factor barrier
        hashMap.put(wrongHashCode,"Test2");

        //Always 2
        System.out.println("Keys count " + hashMap.keySet().size());
    }
}

所以,我的问题是为什么在调整 hashMap 的大小后(据我所知,这涉及 重新散列键),我们在 keySet 中仍然有 2 个键而不是 1 个(因为键对象对于两个现有的 KV 对)?

【问题讨论】:

  • 实际调整大小在哪里?
  • @ACV,当我们添加第二个 KV hashMap 块时,大小变为 4 而不是 2(初始)。还是我错过了什么?
  • hashMap.keySet() 返回哈希映射中实际键的数量。所以,当你放两个键时,你总是得到 2 个。
  • 您是否通过调试确认 HashMap 实际上正在调整大小?
  • @jtahlborn,是的

标签: java hashmap


【解决方案1】:

所以,我的问题是为什么在调整 hashMap 的大小之后(据我所知,这涉及重新散列键)

它实际上涉及重新散列键 - 至少在 HashMap 代码中没有,除非在某些情况下(见下文)。它涉及在地图桶中重新定位它们。 HashMap 内部是一个 Entry 类,它具有以下字段:

final K key;
V value;
Entry<K,V> next;
int hash;

hash 字段是在调用put(...) 时计算的密钥的存储哈希码。这意味着如果您更改对象中的哈希码,它不会影响 HashMap 中的条目,除非您将其重新放入映射中。当然,如果您更改某个键的哈希码,您甚至无法在HashMap找到它,因为它与存储的哈希条目具有不同的哈希码。

我们在 keySet 中仍然有 2 个键而不是 1 个(因为两个现有 KV 对的键对象相同)?

因此,即使您更改了单个对象的哈希值,它仍在映射中,其中包含 2 个条目,其中包含不同的哈希字段。


话虽如此,HashMap 内部有代码可能在调整 HashMap 大小时重新散列键 - 请参阅 jdk 7 中的包保护HashMap.transfer(...) 方法(至少)。这就是为什么上面的hash 字段不是final。但是,它仅在 initHashSeedAsNeeded(...) 返回 true 以使用“替代散列”时使用。以下设置启用 alt-hashing 的条目数阈值:

-Djdk.map.althashing.threshold=1

通过在 VM 上设置此设置,我实际上能够在调整大小时再次调用 hashcode(),但我无法将第二个 put(...) 视为覆盖。部分问题是HashMap.hash(...) 方法正在与内部hashseed 进行异或,当调整大小时会更改,但之后put(...) 记录传入的新哈希码进入。

【讨论】:

  • 我明白了。因此,哈希被缓存,然后被重用,只是为了在新的(扩展或收缩取决于负载因子)哈希图中找到条目的位置,而不是像我们大多数人在算法等中教导的那样实际重新哈希键。类。学到了一些新的和重要的东西。
  • 这当然取决于HashMap的实现。我没有看过(例如)LinkedHashMapMap 的其他实现。没有什么可以说明Map 实现不能多次调用obj.hashcode(),尽管正如@jthlborn 提到的那样,这样做可能会很昂贵。
  • 这就是为什么建议使用不可变的键(或至少等于/哈希码部分)的原因。对于上面的示例,行为是由实现定义的,并且可能因 JVM 而异。这可能是一个思考练习。
  • @Gray HashMap.tranfer?那是什么版本的java?
  • 是jdk 7 @Eugene中HashMap中的封装方法。
【解决方案2】:

HashMap 实际上缓存每个键的 hashCode(因为键的 hashCode 计算起来可能很昂贵)。因此,尽管您更改了现有键的 hashCode,但它在 HashMap 中链接到的 Entry 仍然具有旧代码(因此在调整大小后被放入“错误”的存储桶中)。

您可以在 HashMap.resize() 的 jvm 代码中亲自看到这一点(或者在 java 6 代码 HashMap.transfer() 中更容易看到)。

【讨论】:

  • 我不知道您为什么链接了 jdk-6 源,例如为什么不链接 8?根本没有这样的方法
  • @Eugene 在我搜索 HashMap 的来源时恰好是谷歌上的第一个热门。看java 8,答案还是正确的,虽然从源头破译有点困难。
  • @Eugene 添加 java 8 链接。
【解决方案3】:

例如,当 java-8 中根本不存在该方法时,我不知道为什么其中两个答案依赖于 HashMap.tranfer。因此,我将考虑 java-8 提供我的少量输入。

HashMap 中的条目确实被重新散列,但不是您认为的那样。重新哈希基本上是重新计算(由您)提供的Key#hashcode;有一种方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

所以基本上当你计算你的哈希码时,HashMap 基本上会说 - “我不够信任你”,它会重新哈希你的哈希码并可能更好地传播比特(它是实际上是前 16 位和后 16 位的 XOR)。

另一方面,当HashMap 重新调整大小时,这实际上意味着箱/桶的数量增加了一倍;并且因为垃圾箱总是是 2 的幂 - 这意味着来自当前垃圾箱的条目将: 可能留在同一个桶中 OR 移动到位于当前 bin 数量的偏移量。你可以找到一些细节是如何做到的in this question

因此,一旦发生重新调整大小,就不会再进行额外的重新散列;实际上又考虑了一位,因此条目可能会移动或停留在原处。从这个意义上说,格雷的答案是正确的,每个 Entry 都有 hash 字段,该字段仅计算一次 - 第一次输入 Entry

【讨论】:

    【解决方案4】:

    我找不到清楚的文档,但是以更改其hashCode() 的方式更改键值通常会破坏HashMap

    HashMap 在 b 个桶中划分条目。您可以想象带有哈希h 的键被分配给存储桶h%b。 当它收到一个新条目时,它会计算出它属于哪个桶,然后如果该桶中已经存在相等的键。它最终将它添加到桶中删除任何匹配的键。

    通过更改哈希码,对象wrongHashCode 将(通常在此处实际上)第二次被定向到另一个存储桶,并且不会找到或删除其第一个条目。

    简而言之,更改已插入密钥的哈希会破坏HashMap,之后您会得到不可预测的结果,但可能会导致 (a) 找不到密钥或 (b) 找到两个或更多相等的密钥。

    【讨论】:

      【解决方案5】:

      因为 HashMap 将元素存储在内部表中,并且递增代码不会影响该表:

        public V put(K key, V value) {
              if (key == null)
                  return putForNullKey(value);
              int hash = hash(key.hashCode());
              int i = indexFor(hash, table.length);
              for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                  Object k;
                  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                      V oldValue = e.value;
                      e.value = value;
                      e.recordAccess(this);
                      return oldValue;
                  }
              }
      
              modCount++;
              addEntry(hash, key, value, i);
              return null;
          }
      

      还有addEntry

        void addEntry(int hash, K key, V value, int bucketIndex) {
          Entry<K,V> e = table[bucketIndex];
              table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
              if (size++ >= threshold)
                  resize(2 * table.length);
          }
      

      如您所见table[bucketIndex] = new Entry (hash, ...),因此尽管您增加了代码,但它不会在此处反映出来。

      尝试将字段代码设为Integer,看看会发生什么?

      【讨论】:

      • 是的,但是该表是在 hashMap 调整大小后重建的
      • @VladyslavNikolaiev。不,这不对。哪里说重建了?我在代码中看不到。
      • @ACV4,当元素计数达到特定值时,它会调整大小。在该内表大小加倍(重建)之后
      猜你喜欢
      • 2012-12-24
      • 2017-09-06
      • 2013-03-05
      • 1970-01-01
      • 1970-01-01
      • 2012-10-13
      • 1970-01-01
      • 1970-01-01
      • 2017-08-13
      相关资源
      最近更新 更多