Je comprends que la méthode String ' hashCode () n'est pas garantie de générer des codes de hachage uniques pour des String-s distincts. Je vois beaucoup d'utilisation de la mise des clés String dans HashMap-s (en utilisant la méthode String hashCode () par défaut). Une grande partie de cette utilisation peut entraîner des problèmes d'application importants si une carte put
déplace une entrée HashMap précédemment mise sur la carte avec une clé String vraiment distincte.
Quelles sont les chances que vous exécutiez dans le scénario où String.hashCode () renvoie la même valeur pour des chaînes distinctes? Comment les développeurs peuvent-ils contourner ce problème lorsque la clé est une chaîne?
Les développeurs n'ont pas à contourner le problème des collisions de hachage dans HashMap pour obtenir l'exactitude du programme.
Il y a quelques éléments clés à comprendre ici:
Un peu plus de détails, si vous le souhaitez:
La façon dont le hachage fonctionne (en particulier, dans le cas de collections hachées comme HashMap de Java, ce que vous avez demandé) est la suivante:
Le HashMap stocke les valeurs que vous lui donnez dans une collection de sous-collections, appelées compartiments. Ceux-ci sont en fait implémentés sous forme de listes chaînées. Il y en a un nombre limité: iirc, 16 pour commencer par défaut, et le nombre augmente à mesure que vous mettez plus d'éléments dans la carte. Il doit toujours y avoir plus de compartiments que de valeurs. Pour donner un exemple, en utilisant les valeurs par défaut, si vous ajoutez 100 entrées à un HashMap, il y aura 256 compartiments.
Chaque valeur qui peut être utilisée comme clé dans une carte doit pouvoir générer une valeur entière, appelée code de hachage.
Le HashMap utilise ce code de hachage pour sélectionner un compartiment. En fin de compte, cela signifie prendre la valeur entière modulo
le nombre de compartiments, mais avant cela, le HashMap de Java a une méthode interne (appelée hash()
), qui ajuste le code de hachage pour réduire certaines sources connues de agglomérant.
Lors de la recherche d'une valeur, le HashMap sélectionne le compartiment, puis recherche l'élément individuel par une recherche linéaire de la liste liée, en utilisant .equals()
.
Donc: vous n'avez pas à contourner les collisions pour être correct, et vous n'avez généralement pas à vous en soucier pour les performances, et si vous utilisez des classes natives Java (comme String) , vous n'avez pas non plus à vous soucier de générer les valeurs de code de hachage.
Dans le cas où vous devez écrire votre propre méthode de hachage (ce qui signifie que vous avez écrit une classe avec une valeur composée, comme une paire prénom/nom), les choses deviennent un peu plus compliquées. Il est tout à fait possible de se tromper ici, mais ce n'est pas sorcier. Tout d'abord, sachez ceci: la seule chose que vous devez faire pour garantir l'exactitude est de vous assurer que des objets égaux produisent des codes de hachage égaux. Donc, si vous écrivez une méthode hashcode () pour votre classe, vous devez également écrire une méthode equals (), et vous devez examiner les mêmes valeurs dans chacune.
Il est possible d'écrire une méthode hashcode () qui est mauvaise mais correcte, par laquelle je veux dire qu'elle satisferait à la contrainte "les objets égaux doivent donner des codes de hachage égaux", mais qu'elle fonctionne toujours très mal, en ayant beaucoup de collisions.
Le pire cas dégénéré canonique de ceci serait d'écrire une méthode qui renvoie simplement une valeur constante (par exemple, 3) pour tous les cas. Cela signifierait que chaque valeur serait hachée dans le même compartiment.
Il serait toujours travail, mais les performances se dégraderaient à celles d'une liste chaînée.
Évidemment, vous n'écrirez pas une méthode hashcode () aussi terrible. Si vous utilisez un IDE décent, il est capable d'en générer un pour vous. Puisque StackOverflow aime le code, voici le code de la classe prénom/nom ci-dessus.
public class SimpleName {
private String firstName;
private String lastName;
public SimpleName(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result
+ ((lastName == null) ? 0 : lastName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SimpleName other = (SimpleName) obj;
if (firstName == null) {
if (other.firstName != null)
return false;
} else if (!firstName.equals(other.firstName))
return false;
if (lastName == null) {
if (other.lastName != null)
return false;
} else if (!lastName.equals(other.lastName))
return false;
return true;
}
}
Je soupçonne fortement que le HashMap.put
ne détermine pas si la clé est la même en regardant simplement String.hashCode
.
Il y aura certainement une chance de collision de hachage , donc on peut s'attendre à ce que String.equals
sera également appelée pour être sûr que les String
sont vraiment égaux, s'il y a bien un cas où les deux String
s ont le même valeur renvoyée par hashCode
.
Par conséquent, la nouvelle clé String
ne sera jugée que comme étant la même clé String
que celle qui est déjà dans le HashMap
si et seulement si la valeur retournée par hashCode
est égal et la méthode equals
renvoie true
.
De plus, cette idée serait également vraie pour les classes autres que String
, car la classe Object
elle-même possède déjà la hashCode
et equals
méthodes.
Modifier
Donc, pour répondre à la question, non, ce ne serait pas une mauvaise idée d'utiliser un String
pour une clé d'un HashMap
.
Ce n'est pas un problème, c'est juste le fonctionnement des tables de hachage. Il est impossible de disposer de codes de hachage distincts pour toutes les chaînes distinctes, car il y a beaucoup plus de chaînes distinctes que d'entiers.
Comme d'autres l'ont écrit, les collisions de hachage sont résolues via la méthode equals (). Le seul problème que cela peut provoquer est la dégénérescence de la table de hachage, conduisant à de mauvaises performances. C'est pourquoi HashMap de Java a un facteur de charge , un rapport entre les compartiments et les éléments insérés qui, lorsqu'il est dépassé, provoquera le ressassement de la table avec deux fois le nombre de compartiments.
Cela fonctionne généralement très bien, mais uniquement si la fonction de hachage est bonne, c'est-à-dire qu'elle n'entraîne pas plus que le nombre de collisions statistiquement attendu pour votre ensemble d'entrée particulier. String.hashCode()
est bon à cet égard, mais il n'en a pas toujours été ainsi. Prétendument , avant Java 1.2, il n'incluait que chaque nième caractère. Cela était plus rapide, mais provoquait des collisions prévisibles pour toutes les chaînes partageant chaque nième caractère - très mauvais si vous manquez de chance pour avoir une telle entrée régulière, ou si quelqu'un veut faire une attaque DOS sur votre application.
Je vous dirige vers la réponse ici . Bien que ce ne soit pas une idée mauvaise d'utiliser des chaînes (@CPerkins a expliqué pourquoi, parfaitement), le stockage des valeurs dans une table de hachage avec des clés entières est mieux, car il est généralement plus rapide (bien que de façon imperceptible) et a moins de chances (en fait, aucune chance) de collisions.
Voir ce tableau des collisions utilisant 216553 clés dans chaque cas, (volé dans ce post , reformaté pour notre discussion)
Hash Lowercase Random UUID Numbers ============= ============= =========== ============== Murmur 145 ns 259 ns 92 ns 6 collis 5 collis 0 collis FNV-1a 152 ns 504 ns 86 ns 4 collis 4 collis 0 collis FNV-1 184 ns 730 ns 92 ns 1 collis 5 collis 0 collis* DBJ2a 158 ns 443 ns 91 ns 5 collis 6 collis 0 collis*** DJB2 156 ns 437 ns 93 ns 7 collis 6 collis 0 collis*** SDBM 148 ns 484 ns 90 ns 4 collis 6 collis 0 collis** CRC32 250 ns 946 ns 130 ns 2 collis 0 collis 0 collis Avg Time per key 0.8ps 2.5ps 0.44ps Collisions (%) 0.002% 0.002% 0%
Bien sûr, le nombre d'entiers est limité à 2 ^ 32, où comme il n'y a pas de limite au nombre de chaînes (et il n'y a pas de limite théorique à la quantité de clés pouvant être stockées dans un HashMap
) . Si vous utilisez un long
(ou même un float
), les collisions seront inévitables, et donc pas "meilleures" qu'une chaîne. Cependant, même malgré les collisions de hachage, put()
et get()
placera/obtiendra toujours la paire clé-valeur correcte (voir modification ci-dessous).
En fin de compte, cela n'a vraiment pas d'importance, alors utilisez ce qui est plus pratique. Mais si la commodité ne fait aucune différence et que vous n'avez pas l'intention d'avoir plus de 2 ^ 32 entrées, je vous suggère d'utiliser ints
comme clés.
[~ # ~] modifier [~ # ~]
Bien que ce qui précède soit certainement vrai, n'utilisez JAMAIS "StringKey" .hashCode () pour générer une clé à la place de la clé String
d'origine pour des raisons de performances - 2 chaînes différentes peuvent avoir le même hashCode, provoquant l'écrasement de votre put()
méthode. L'implémentation Java de HashMap
est assez intelligente pour gérer automatiquement les chaînes (n'importe quel type de clé) avec le même code de hachage, il est donc judicieux de laisser Java gérer ces choses pour vous .
Vous parlez de collisions de hachage. Les collisions de hachage sont un problème quel que soit le type de hashCode'd. Toutes les classes qui utilisent hashCode (par exemple HashMap) gèrent très bien les collisions de hachage. Par exemple, HashMap peut stocker plusieurs objets par compartiment.
Ne vous en faites pas sauf si vous appelez vous-même hashCode. Les collisions de hachage, bien que rares, ne cassent rien.