Supposons que nous avons du code
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());
}
}
Ma question est donc de savoir pourquoi, après avoir redimensionné hashMap (qui, si je comprends bien, implique rehashing keys), nous avons toujours 2 clés dans keySet au lieu de 1 (puisque l’objet clé est identique pour les deux paires KV existantes)?
Donc, ma question est de savoir pourquoi après le redimensionnement de hashMap (qui, autant que je sache, implique de ressasser des clés)
En fait, il s'agit de non impliquent des clés de réémaillage - du moins pas dans le code HashMap
, sauf dans certaines circonstances (voir ci-dessous). Cela implique de les repositionner dans les compartiments de la carte. À l'intérieur de HashMap
se trouve une classe Entry
qui comporte les champs suivants:
final K key;
V value;
Entry<K,V> next;
int hash;
Le champ hash
est le code de hachage stocké pour la clé qui est calculé lors de l'appel put(...)
. Cela signifie que si vous modifiez le code de hachage dans votre objet, cela n'affectera pas l'entrée dans la carte de hachage, à moins que vous ne le réintroduisiez dans la carte. Bien sûr, si vous modifiez le code de hachage pour une clé, vous ne pourrez même pas le trouver dans HashMap
car il a un code de hachage différent de celui de l'entrée de hachage stockée.
nous avons toujours 2 clés dans keySet au lieu de 1 (puisque l'objet clé est identique pour les deux paires KV existantes)?
Ainsi, même si vous avez modifié le hachage pour un seul objet, il se trouve dans la carte avec 2 entrées contenant différents champs de hachage.
Cela dit, il y a du code à l'intérieur de HashMap
qui peut ressaisir les clés lorsqu'un HashMap est redimensionné - voir la méthode package protected HashMap.transfer(...)
dans jdk 7 (au moins). C'est pourquoi le champ hash
ci-dessus n'est pas final
. Cependant, il n'est utilisé que lorsque initHashSeedAsNeeded(...)
renvoie true pour utiliser le "hachage alternatif". Les éléments suivants définissent le seuil du nombre d'entrées pour lesquelles le hachage alternatif est activé:
-Djdk.map.althashing.threshold=1
Avec cet ensemble sur la machine virtuelle, je suis en mesure d'obtenir à nouveau l'appel de la hashcode()
lorsque le redimensionnement est effectué, mais je ne parviens pas à faire en sorte que la 2ème put(...)
soit considérée comme un écrasement. Une partie du problème est que la méthode HashMap.hash(...)
effectue un XOR avec le hashseed
interne qui est modifié lorsque le redimensionnement est effectué, mais après le put(...)
enregistre le nouveau code de hachage pour l'entrée entrante.
Le HashMap en fait caches le hashCode pour chaque clé (car le hashCode d'une clé peut être coûteux à calculer). Ainsi, bien que vous ayez changé le hashCode pour une clé existante, l’entrée à laquelle elle est liée dans HashMap conserve l’ancien code (et est donc placée dans le "mauvais" compartiment après le redimensionnement).
Vous pouvez le voir vous-même dans le code jvm de HashMap.resize () (ou un peu plus facile à voir dans le code Java 6 - HashMap.transfer () ).
Je ne parviens pas à le trouver clairement documenté, mais le fait de modifier une valeur clé d'une manière qui modifie sa hashCode()
rompt généralement une HashMap
.
HashMap
divise les entrées entre b compartiments. Vous pouvez imaginer que la clé avec hash h
soit affectée au compartiment h%b
. Lorsqu'il reçoit une nouvelle entrée, il détermine le compartiment auquel elle appartient, si une clé égale existe déjà dans ce compartiment. Il l'ajoute enfin au panier en supprimant toute clé correspondante.
En modifiant le code de hachage, l'objet wrongHashCode
sera (généralement et ici en fait) dirigé vers un autre compartiment une seconde fois et sa première entrée ne sera ni trouvée ni supprimée.
En bref, changer le hash d'une clé déjà insérée rompt la HashMap
et ce que vous obtenez après est imprévisible, mais peut avoir pour conséquence (a) de ne pas trouver une clé ou (b) de trouver deux clés égales ou plus.
Je ne peux pas dire pourquoi deux des réponses s'appuient sur HashMap.tranfer
, par exemple, alors que cette méthode n'est pas présente dans Java-8. En tant que tel, je fournirai ma petite contribution en prenant Java-8 en considération.
Les entrées dans une HashMap
sont en effet hachées, mais pas dans le sens que vous pourriez penser. Un re-hachage consiste à recalculer le déjà fourni (par vous) du Key#hashcode
; il existe une méthode pour cela:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Donc lorsque vous calculez votre hashcode, HashMap
dira en gros - "je ne vous fais pas suffisamment confiance" et il va re-hash votre hashcode et potentiellement mieux répartir les bits (c'est en fait une XOR
des 16 premiers bits et les 16 derniers bits).
D'autre part, lorsque HashMap
est redimensionné, cela signifie en fait que le nombre de compartiments/compartiments est doublé; et parce que les bacs sont toujours une puissance de deux, cela signifie qu'une entrée d'un bac actuel va: le potentiel rester dans le même compartiment OU se déplacer dans un compartiment qui est décalé au niveau du courant nombre de bacs. Vous pouvez trouver un peu de détails sur la manière de procéder dans cette question .
Ainsi, une fois la redimensionnement effectué, il n'y a plus de re-hachage; en réalité, un bit supplémentaire est pris en compte et une entrée peut donc être déplacée ou rester telle quelle. Et la réponse de Gray est correcte en ce sens que chaque Entry
a le champ hash
, qui est calculé une seule fois - la première fois que vous mettez cette Entry
.
Parce que HashMap stocke les éléments dans une table interne et que l'incrémentation du code n'affecte pas cette table:
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;
}
Et 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);
}
Comme vous pouvez voir table[bucketIndex] = new Entry (hash, ...)
, bien que vous incrémentiez le code, il ne sera pas reflété ici.
Essayez de transformer le code de champ en Integer
et voyez ce qui se passe?