web-dev-qa-db-fra.com

Mauvaise idée d'utiliser la clé de chaîne dans HashMap?

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?

66
Marcus Leon

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:

  1. Les collisions sont une caractéristique inhérente du hachage, et elles doivent l'être. Le nombre de valeurs possibles (des chaînes dans votre cas, mais cela s'applique également à d'autres types) est beaucoup plus grand que la plage d'entiers.
  2. Chaque utilisation du hachage a un moyen de gérer les collisions, et les collections Java (y compris HashMap) ne font pas exception.
  3. Le hachage n'est pas impliqué dans les tests d'égalité. Il est vrai que des objets égaux doivent avoir des codes de hachage égaux, mais l'inverse n'est pas vrai: de nombreuses valeurs auront le même code de hachage. N'essayez donc pas d'utiliser une comparaison de hashcode comme substitut de l'égalité. Les collections ne le font pas. Ils utilisent le hachage pour sélectionner une sous-collection (appelée un compartiment dans le monde Java Collections), mais ils utilisent .equals () pour vérifier réellement l'égalité.
  4. Non seulement vous n'avez pas à vous soucier des collisions entraînant des résultats incorrects dans une collection, mais pour la plupart des applications, vous n'avez également * généralement * pas à vous soucier des performances - Java les collections hachées font une assez bon travail de gestion des codes de hachage.
  5. Mieux encore, dans le cas que vous avez demandé (Strings comme clés), vous n'avez même pas à vous soucier des codes de hachage eux-mêmes, car la classe String de Java génère un très bon code de hachage. Il en va de même pour la plupart des classes Java Java fournies).

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;
    }
}

113
CPerkins

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 Strings 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.

4
coobird

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.

4

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 .

4
dberm22

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.

2
Keith Randall