Java HashMap调整大小

时间:2017-09-05 13:23:32

标签: java hashmap

我们假设我们有一些代码

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的大小之后(据我所理解的那样,涉及 rehashing keys ),我们仍然在keySet中有2个键而不是1个(因为key对象是相同的现有的KV对)?

5 个答案:

答案 0 :(得分:8)

  

所以,我的问题是为什么在调整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(...)方法(at最小)。这就是上面的hash字段不是final的原因。仅当initHashSeedAsNeeded(...)返回true以使用“替代散列”时才使用它。以下设置启用alt-hashing的条目数阈值:

-Djdk.map.althashing.threshold=1

在VM上使用此设置,我实际上能够在调整大小时再次调用hashcode(),但我无法将第二个put(...)视为覆盖。问题的一部分是HashMap.hash(...)方法正在对内部hashseed执行XOR,在调整大小时会更改,但put(...)之后传入条目的新哈希码。

答案 1 :(得分:7)

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

你可以在HashMap.resize()的jvm代码中看到这个(或者在java 6代码HashMap.transfer()中更容易看到)。

答案 2 :(得分:2)

我无法清楚地记录下来,但以更改其hashCode()的方式更改键值通常会导致HashMap

HashMap在b个桶之间划分条目。您可以想象将哈希h的密钥分配给存储桶h%b。 当它收到一个新条目时,它会计算出它所属的那个桶,如果该桶中已存在相同的密钥。它最终将它添加到存储桶中,删除任何匹配的密钥。

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

简而言之,更改已插入密钥的散列会破坏HashMap,之后得到的内容是不可预测的,但可能导致(a)找不到密钥或(b)找到两个或更多相等的密钥

答案 3 :(得分:2)

我不知道为什么两个答案依赖HashMap.tranfer作为一些例子,当java-8中根本不存在该方法时。因此,我将提供考虑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的幂 - 这意味着来自当前垃圾箱的条目将:潜在停留在同一个桶移动到位于在当前的箱数偏移。你可以找到一些细节如何完成in this question

因此,一旦重新调整大小,就不会有额外的重新散列;实际上还有一点被考虑在内,因此一个条目可能会移动或保持原样。从这个意义上说,Gray的答案是正确的,每个Entry都有hash字段,只计算一次 - 第一次放置Entry

答案 4 :(得分:0)

因为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,看看会发生什么?