web-dev-qa-db-fra.com

Pourquoi String.hashCode () dans Java a-t-il de nombreux conflits?

Pourquoi String.hashcode () a-t-il autant de conflits?

Je lis le String.hashCode () dans jdk1.6, ci-dessous les codes

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

Cela me semble assez déroutant car il y a tellement de conflits; bien qu'il ne soit pas nécessaire d'être unique (nous pouvons toujours compter sur equals ()), mais moins de conflits signifie de meilleures performances sans visiter les entrées d'une liste chaînée.

Supposons que nous ayons deux caractères, alors tant que nous pouvons trouver deux chaînes correspondant à l'équation ci-dessous, nous aurons alors le même hashcode ()

a * 31 +b = c * 31 +d

Il sera facile de conclure que (a-c) * 31 = d-b prenons un exemple simple: make a-c = 1 et d-b = 31; donc j'ai écrit ci-dessous les codes pour un test simple

public void testHash() {
    System.out.println("A:" + (int)'A');
    System.out.println("B:" + (int)'B');
    System.out.println("a:" + (int)'a');

    System.out.println("Aa".hashCode() + "," + "BB".hashCode());
    System.out.println("Ba".hashCode() + "," + "CB".hashCode());
    System.out.println("Ca".hashCode() + "," + "DB".hashCode());
    System.out.println("Da".hashCode() + "," + "EB".hashCode());        
}

il affichera les résultats ci-dessous, ce qui signifie que toutes les chaînes ont le même hashcode (), et il est facile de le faire en boucle.

A:65 
B:66
a:97
2112,2112
2143,2143
2174,2174
2205,2205

pire encore, supposons que nous ayons 4 caractères dans la chaîne, selon l'algorithme, supposons que les 2 premiers caractères produisent a2, les 2ièmes 2 caractères produisent b2; le code de hachage sera toujours a2 * 31^2 + b2 ainsi, avec a2 et b2 égaux entre 2 chaînes, nous aurons plus de chaînes avec le conflit hashcode (). ces exemples sont "AaAa", "BBBB" et ainsi de suite; alors nous aurons 6 caractères, 8 caractères ......

supposons que la plupart du temps, nous utilisons des caractères dans la table ascii dans une chaîne qui sera utilisée dans une table de hachage ou une table de hachage, alors le nombre premier 31 choisi ici est définitivement trop petit;

une solution simple consiste à utiliser un nombre premier plus grand (heureusement, 257 est un nombre premier) qui peut éviter ce conflit. bien sûr, choisir un nombre trop grand entraînera un débordement de la valeur int retournée si la chaîne est très longue, mais je suppose que la plupart du temps, la chaîne utilisée comme clé n'est pas si grande? bien sûr, il pourrait toujours renvoyer une valeur longue pour éviter cela.

ci-dessous est ma version modifiée de betterhash () qui peut résoudre ces conflits facilement en exécutant les codes, il affichera les valeurs ci-dessous, ce qui est efficace pour résoudre ce problème.

16802,17028
17059,17285
17316,17542
17573,17799

mais pourquoi jdk ne le résout pas? THX.

@Test
public void testBetterhash() {
    System.out.println(betterHash("Aa") + "," + betterHash("BB"));      
    System.out.println(betterHash("Ba") + "," + betterHash("CB"));
    System.out.println(betterHash("Ca") + "," + betterHash("DB"));
    System.out.println(betterHash("Da") + "," + betterHash("EB"));
}

public static int betterHash(String s) {
    int h = 0;
    int len = s.length();

    for (int i = 0; i < len; i++) {
        h = 257*h + s.charAt(i);
    }
    return h;
}
29
hetaoblog

Je viens de hacher 58 000 mots de langue anglaise (trouvés ici ), tous en minuscules et également avec la première lettre en majuscule. Vous savez combien sont entrés en collision? Deux: "Frères et sœurs" et "Téhéran" (une orthographe alternative de "Téhéran").

Tout comme vous, j'ai pris un sous-domaine (dans mon cas, probablement un) de chaînes possibles et analysé le taux de collision de hashCode pour lui, et je l'ai trouvé exemplaire. Qui peut dire que votre sous-domaine arbitraire de chaînes possibles est un meilleur choix à optimiser que le mien?

Les personnes qui ont écrit cette classe ont dû le faire en sachant qu'elles ne pouvaient pas prédire (ni donc optimiser) le sous-domaine dans lequel leurs utilisateurs utiliseraient des chaînes comme clés. Ils ont donc choisi une fonction de hachage qui se répartit uniformément sur tout le domaine des chaînes.

Si vous êtes intéressé, voici mon code (il utilise la goyave):

    List<String> words = CharStreams.readLines(new InputStreamReader(StringHashTester.class.getResourceAsStream("corncob_lowercase.txt")));
    Multimap<Integer, String> wordMap = ArrayListMultimap.create();
    for (String Word : words) {
        wordMap.put(Word.hashCode(), Word);
        String capitalizedWord = Word.substring(0, 1).toUpperCase() + Word.substring(1);
        wordMap.put(capitalizedWord.hashCode(), capitalizedWord);
    }

    Map<Integer, Collection<String>> collisions = Maps.filterValues(wordMap.asMap(), new Predicate<Collection<String>>() {
        public boolean apply(Collection<String> strings) {
            return strings.size() > 1;
        }
    });

    System.out.println("Number of collisions: " + collisions.size());
    for (Collection<String> collision : collisions.values()) {
        System.out.println(collision);
    }

Éditer

Soit dit en passant, si vous êtes curieux, le même test avec votre fonction de hachage a eu 13 collisions par rapport à String.hashCode's 1.

43
Mark Peters

Je suis désolé, mais nous devons jeter un peu d'eau froide sur cette idée.

  1. Votre analyse est beaucoup trop simpliste. Vous semblez avoir choisi un sous-ensemble de cordes conçu pour prouver votre point. Cela ne prouve pas que le nombre de collisions est (statistiquement) plus élevé que prévu dans le domaine de toutes les chaînes.

  2. Personne dans son bon sens ne voudrait s'attendre String.hashCode être hautement sans collision. Il n'est tout simplement pas conçu dans cet esprit. (Si vous voulez un hachage hautement sans collision, utilisez un algorithme de hachage cryptographique ... et payez le coût.) String.hashCode () est conçu pour être raisonnablement bon dans le domaine de toutes les chaînes ... et rapide .

  3. En supposant que vous pourriez déclarer un cas plus solide, ce n'est pas le lieu de le déclarer. Vous devez soulever ce problème avec les personnes qui comptent - l'équipe d'ingénierie Java Java.

  4. L'équipe Java ingénierie va peser les avantages d'un tel changement par rapport aux coûts de sa mise en œuvre, pour eux, et pour tous les autres utilisateurs de Java. . Le dernier point est probablement suffisant pour tuer cette idée de pierre morte.


("Hachage hautement sans collision", est une idée/un terme que j'ai retiré de l'air aux fins de cette réponse. Désolé. Cependant, l'essentiel est que la probabilité d'une collision de code de hachage pour 2 chaînes devrait être indépendante de la façon dont elle est liée. ils sont. Ainsi, par exemple. "AA" et "bz" sont liés en raison de la même longueur. De toute évidence, cette idée nécessite plus de réflexion. Et il est également évident que la "parenté" dans le sens dont je parle est non mesurable ... un peu comme Complexité de Kolmogorov .)

13
Stephen C

Les collisions sont inévitables lors du hachage. La méthode hashCode() renvoie un entier qui est utilisé comme index dans un tableau qui est un compartiment pour tous les objets avec le même code de hachage. La méthode equals(Object) est utilisée pour comparer l'objet cible avec chacun dans le compartiment pour identifier l'objet correspondant exactement, s'il existe.

En fin de compte, la méthode hashCode() doit simplement être rapide et pas trop faible (c'est-à-dire provoquer trop de collisions), où trop faible est une métrique assez floue .

9
maerics

C'est assez efficace mais aussi simple. Tous les mots minuscules (ASCII) pouvant contenir jusqu'à six lettres ou tous les nombres jusqu'à six chiffres ont un hashCode () unique. c'est-à-dire que le hashCode est comme un nombre de base 31. L'utilisation d'un plus grand nombre a ses propres problèmes. Un facteur 257 laisserait tous les 8 bits pas particulièrement aléatoire car tous les caractères ASCII ont un bit supérieur 0. Un facteur plus grand entraînerait des codes de hachage en double pour les mots à cinq et six chiffres/lettres.

Quel est peut-être le plus gros problème si vous ne pouvez pas modifier l'algorithme de hachage. Quelle que soit l'approche que vous adoptez, il peut y avoir un cas où c'est un très mauvais choix et il est probable qu'il ne soit pas optimal pour votre cas d'utilisation.

Le plus gros problème est peut-être les attaques par déni de service rendant les cas pathologiques, normalement très rares assez courants. Par exemple, un moyen d'attaquer un serveur Web consiste à remplir un cache avec des clés toutes avec le même hashCode, par ex. 0 qui est calculé à chaque fois. Cela entraîne la dégénérescence de HashMap en une liste liée.

Un moyen simple de contourner cela est de rendre l'algorithme de hachage inconnu, éventuellement en train de changer. En l'état, le mieux pourrait être d'utiliser un TreeMap (qui prend en charge la comparaison personnalisée, bien que la valeur par défaut convienne dans ce cas)

1
Peter Lawrey