我们假设我们有一些代码
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对)?
答案 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
,看看会发生什么?