web-dev-qa-db-fra.com

Qu'est-ce qu'une bonne fonction de hachage 64 bits dans Java pour les chaînes textuelles?

Je recherche une fonction de hachage qui:

  1. Hashs chaînes textuelles bien (par exemple, peu de collisions)
  2. Est écrit en Java et largement utilisé
  3. Bonus: fonctionne sur plusieurs champs (au lieu de les concaténer et d'appliquer le hachage sur la chaîne concaténée)
  4. Bonus: a une variante de 128 bits.
  5. Bonus: pas gourmand en CPU.
55
ripper234

Pourquoi n'utilisez-vous pas une variante long de la fonction par défaut String.hashCode() (où certains gars vraiment intelligents font certainement des efforts pour la rendre efficace - sans parler des milliers d'yeux de développeurs qui ont déjà examiné cette code)?

// adapted from String.hashCode()
public static long hash(String string) {
  long h = 1125899906842597L; // prime
  int len = string.length();

  for (int i = 0; i < len; i++) {
    h = 31*h + string.charAt(i);
  }
  return h;
}

Si vous cherchez encore plus de bits, vous pourriez probablement utiliser un BigInteger Éditer:

Comme je l'ai mentionné dans un commentaire à la réponse de @brianegge, il n'y a pas beaucoup de cas d'utilisation pour les hachages avec plus de 32 bits et probablement pas un seul pour les hachages avec plus de 64 bits:

Je pourrais imaginer une énorme table de hachage répartie sur des dizaines de serveurs, stockant peut-être des dizaines de milliards de mappages. Pour un tel scénario, @brianegge a toujours un point valide ici: 32 bits autorisent 2 ^ 32 (environ 4,3 milliards) de clés de hachage différentes. En supposant un algorithme puissant, vous devriez toujours avoir assez peu de collisions. Avec 64 bits (18 446 744 073 milliards de clés différentes), vous économiserez certainement, quel que soit le scénario fou pour lequel vous en avez besoin. Penser à des cas d'utilisation pour des clés de 128 bits (340 282 366 920 938 463 463 374 607 431 milliards de clés possibles) est quasiment impossible.

Pour combiner le hachage pour plusieurs champs, simplement faire un XOR multipliez un par un nombre premier et ajoutez-les:

long hash = MyHash.hash(string1) * 31 + MyHash.hash(string2);

Le petit nombre premier est là pour éviter un code de hachage égal pour les valeurs commutées, c'est-à-dire {'foo', 'bar'} et {'bar', 'foo'} ne sont pas égaux et doivent avoir un code de hachage différent. XOR est mauvais car il renvoie 0 si les deux valeurs sont égales. Par conséquent, {'foo', 'foo'} et {'bar', 'bar'} auraient le même code de hachage.

64
sfussenegger

Créer un hachage SHA-1 puis masquer les 64 bits les plus bas.

4
Aaron Digulla
long hash = string.hashCode();

Oui, les 32 premiers bits seront 0, mais vous manquerez probablement de ressources matérielles avant de rencontrer des problèmes avec les collisions de hachage. Le hashCode dans String est assez efficace et bien testé.

pdate Je pense que ce qui précède satisfait la chose la plus simple qui pourrait éventuellement fonctionner , cependant, je suis d'accord avec l'idée de @sfussenegger d'étendre la hashCode String existant.

En plus d'avoir un bon hashCode pour votre chaîne, vous voudrez peut-être envisager de ressasser le code de hachage dans votre implémentation. Si votre stockage est utilisé par d'autres développeurs ou utilisé avec d'autres types, cela peut aider à distribuer vos clés. Par exemple, le HashMap de Java est basé sur des tables de hachage de puissance de deux, il ajoute donc cette fonction pour garantir que les bits inférieurs sont suffisamment distribués.

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
3
brianegge

Pourquoi ne pas utiliser un polynôme CRC64. Ceux-ci sont raisonnablement efficaces et optimisés pour s'assurer que tous les bits sont comptés et répartis sur l'espace de résultat.

Il y a beaucoup d'implémentations disponibles sur le net si vous google "CRC64 Java"

2
Peter Tillemans

Inversez la chaîne pour obtenir un autre code de hachage 32 bits, puis combinez les deux:

String s = "astring";
long upper = ( (long) s.hashCode() ) << 32;
long lower = ( (long) s.reverse().hashCode() ) - ( (long) Integer.MIN_VALUE );
long hash64 = upper + lower;

C'est un pseudocode; la méthode String.reverse() n'existe pas et devra être implémentée d'une autre manière.

1
user2020240

Une réponse pour aujourd'hui (2018). SipHash.

Il sera beaucoup plus rapide que la plupart des réponses ici, et d'une qualité nettement supérieure à toutes.

La bibliothèque Guava en possède un: https://google.github.io/guava/releases/23.0/api/docs/com/google/common/hash/Hashing.html#sipHash24--

1
Scott Carey

Faites quelque chose comme ça:

import Java.io.ByteArrayOutputStream;
import Java.io.DataOutputStream;
import Java.io.IOException;
import Java.math.BigInteger;
import Java.security.MessageDigest;
import Java.security.NoSuchAlgorithmException;

public class Test {

    public static void main(String[] args) throws NoSuchAlgorithmException,
            IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);

        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            SomeObject testObject = new SomeObject();

            dos.writeInt(testObject.count);
            dos.writeLong(testObject.product);
            dos.writeDouble(testObject.stdDev);
            dos.writeUTF(testObject.name);
            dos.writeChar(testObject.delimiter);
            dos.flush();

            byte[] hashBytes = md.digest(baos.toByteArray());
            BigInteger testObjectHash = new BigInteger(hashBytes);

            System.out.println("Hash " + testObjectHash);
        } finally {
            dos.close();
        }
    }

    private static class SomeObject {
        private int count = 200;
        private long product = 1235134123l;
        private double stdDev = 12343521.456d;
        private String name = "Test Name";
        private char delimiter = '\n';
    }
}

DataOutputStream vous permet d'écrire des primitives et des chaînes et de les faire sortir en octets. Envelopper un ByteArrayOutputStream dedans vous permettra d'écrire dans un tableau d'octets, qui s'intègre bien avec MessageDigest . Vous pouvez choisir parmi n'importe quel algorithme répertorié ici .

Enfin BigInteger vous permettra de transformer les octets de sortie en un nombre plus facile à utiliser. Les algorithmes MD5 et SHA1 produisent tous les deux des hachages 128 bits, donc si vous en avez besoin de 64, vous pouvez simplement tronquer.

SHA1 devrait hacher presque n'importe quoi et avec des collisions peu fréquentes (c'est 128 bits). Cela fonctionne à partir de Java, mais je ne sais pas comment il est mis en œuvre. Cela peut en fait être assez rapide. Cela fonctionne sur plusieurs domaines dans mon implémentation: il suffit de les pousser tous sur le DataOutputStream et vous êtes prêt à partir. Vous pouvez même le faire avec une réflexion et des annotations (peut-être @HashComponent(order=1) pour montrer quels champs entrent dans un hachage et dans quel ordre). Il a une variante de 128 bits et je pense que vous constaterez qu'il n'utilise pas autant de CPU que vous le pensez.

J'ai utilisé du code comme celui-ci pour obtenir des hachages pour d'énormes ensembles de données (maintenant probablement des milliards d'objets) afin de pouvoir les partager dans de nombreux magasins d'arrière-plan. Cela devrait fonctionner pour tout ce dont vous avez besoin. Notez que je pense que vous ne souhaiterez peut-être appeler MessageDigest.getInstance() qu'une seule fois puis clone() à partir de là: IIRC le clonage est beaucoup plus rapide.

1
jasonmp85

Regardez-vous Apache commons lang ?

Mais pour 64 bits (et 128), vous avez besoin de quelques astuces: les règles énoncées dans le livre Effective Java par Joshua Bloch vous aident à créer facilement un hachage 64 bits (utilisez simplement long au lieu de int). Pour 128 bits, vous avez besoin de hacks supplémentaires ...

0
St.Shadow