Guide pour hashCode () en Java

Guide de hashCode () en Java

1. Vue d'ensemble

Le hachage est un concept fondamental de l'informatique.

En Java, des algorithmes de hachage efficaces se cachent derrière certaines des collections les plus populaires dont nous disposons - telles que lesHashMap (pour un examen approfondi deHashMap, n'hésitez pas à vérifierthis article) et lesHashSet.

Dans cet article, nous allons nous concentrer sur le fonctionnement dehashCode(), comment il joue dans les collections et comment l'implémenter correctement.

2. Utilisation dehashCode() dans les structures de données

Les opérations les plus simples sur les collections peuvent être inefficaces dans certaines situations.

Par exemple, cela déclenche une recherche linéaire qui est très inefficace pour les listes de très grandes tailles:

List words = Arrays.asList("Welcome", "to", "example");
if (words.contains("example")) {
    System.out.println("example is in the list");
}

Java fournit un certain nombre de structures de données pour traiter spécifiquement ce problème - par exemple, plusieurs implémentations d'interfaceMap sonthash tables.

Lorsque vous utilisez une table de hachage,these collections calculate the hash value for a given key using the hashCode() method et utilisez cette valeur en interne pour stocker les données - afin que les opérations d'accès soient beaucoup plus efficaces.

3. Comprendre le fonctionnement dehashCode()

En termes simples,hashCode() renvoie une valeur entière, générée par un algorithme de hachage.

Les objets égaux (selon leurequals()) doivent renvoyer le même code de hachage. It’s not required for different objects to return different hash codes.

Le contrat général dehashCode() stipule:

  • Chaque fois qu'il est invoqué sur le même objet plus d'une fois pendant l'exécution d'une application Java,hashCode() doit systématiquement renvoyer la même valeur, à condition qu'aucune information utilisée dans les comparaisons égales sur l'objet ne soit modifiée. Cette valeur ne doit pas rester cohérente d'une exécution d'une application à une autre exécution de la même application

  • Si deux objets sont égaux selon la méthodeequals(Object), alors l'appel de la méthodehashCode() sur chacun des deux objets doit produire la même valeur

  • Il n'est pas nécessaire que si deux objets sont inégaux selon la méthodeequals(java.lang.Object), l'appel de la méthodehashCode sur chacun des deux objets doit produire des résultats entiers distincts. Toutefois, les développeurs doivent savoir que la production de résultats entiers distincts pour des objets inégaux améliore les performances des tables de hachage.

«Autant que cela soit raisonnablement pratique, la méthodehashCode() définie par la classeObject renvoie des entiers distincts pour des objets distincts. (Ceci est généralement implémenté en convertissant l'adresse interne de l'objet en un entier, mais cette technique d'implémentation n'est pas requise par le langage de programmation JavaTM.) "

4. Une implémentation naïve dehashCode()

Il est en fait assez simple d’avoir une implémentation naïve dehashCode() qui adhère pleinement au contrat ci-dessus.

Pour illustrer cela, nous allons définir un exemple de classeUser qui remplace l'implémentation par défaut de la méthode:

public class User {

    private long id;
    private String name;
    private String email;

    // standard getters/setters/constructors

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
          && (name.equals(user.name)
          && email.equals(user.email));
    }

    // getters and setters here
}

La classeUser fournit des implémentations personnalisées pour lesequals() ethashCode() qui adhèrent pleinement aux contrats respectifs. De plus, il n’ya rien d’illégitime à ce quehashCode() renvoie une valeur fixe.

Cependant, cette implémentation dégrade la fonctionnalité des tables de hachage à pratiquement zéro, car chaque objet serait stocké dans le même compartiment unique.

Dans ce contexte, une recherche dans une table de hachage est effectuée de manière linéaire et ne nous donne aucun avantage réel. Pour plus d'informations à ce sujet, reportez-vous à la section 7.

5. Amélioration de l'implémentation dehashCode()

Améliorons un peu l'implémentation actuelle dehashCode() en incluant tous les champs de la classeUser afin qu'elle puisse produire des résultats différents pour des objets inégaux:

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

Cet algorithme de hachage de base est définitivement bien meilleur que le précédent, car il calcule le code de hachage de l’objet en multipliant simplement les codes de hachage des champsname etemail et desid.

En termes généraux, nous pouvons dire que c'est une implémentation dehashCode() raisonnable, tant que nous gardons l'implémentation deequals() cohérente avec elle.

6. Implémentations standard dehashCode()

Plus l'algorithme de hachage utilisé pour calculer les codes de hachage est performant, meilleures seront les performances des tables de hachage.

Jetons un coup d'œil à une implémentation «standard» qui utilise deux nombres premiers pour ajouter encore plus d'unicité aux codes de hachage calculés:

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

Bien qu'il soit essentiel de comprendre les rôles que jouent les méthodeshashCode() etequals(), nous n'avons pas à les implémenter à partir de zéro à chaque fois, car la plupart des IDE peuvent générer deshashCode() etequals()personnalisés ) et depuis Java 7, nous avons une méthode utilitaireObjects.hash() pour un hachage confortable:

Objects.hash(name, email)

IntelliJ IDEA génère l'implémentation suivante:

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 * result + name.hashCode();
    result = 31 * result + email.hashCode();
    return result;
}

EtEclipse produit celui-ci:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((email == null) ? 0 : email.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

En plus des implémentationshashCode() basées sur l'IDE ci-dessus, il est également possible de générer automatiquement une implémentation efficace, par exemple en utilisantLombok. Dans ce cas, la dépendancelombok-maven doit être ajoutée àpom.xml:


    org.projectlombok
    lombok-maven
    1.16.18.0
    pom

Il suffit maintenant d'annoter la classeUser avec@EqualsAndHashCode:

@EqualsAndHashCode
public class User {
    // fields and methods here
}

De même, si nous voulons queApache Commons Lang’s HashCodeBuilder class génère une implémentation dehashCode() pour nous, la dépendance Mavencommons-lang doit être incluse dans le fichier pom:


    commons-lang
    commons-lang
    2.6

EthashCode() peut être implémenté comme ceci:

public class User {
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
        append(id).
        append(name).
        append(email).
        toHashCode();
    }
}

En général, il n’ya pas de recette universelle à laquelle s’en tenir pour implémenterhashCode(). Nous vous recommandons vivement de lireJoshua Bloch’s Effective Java, qui fournit une liste dethorough guidelines pour implémenter des algorithmes de hachage efficaces.

Ce qui peut être remarqué ici est que toutes ces implémentations utilisent le numéro 31 sous une forme quelconque - c'est parce que 31 a une propriété intéressante - sa multiplication peut être remplacée par un décalage au niveau du bit qui est plus rapide que la multiplication standard:

31 * i == (i << 5) - i

7. Gestion des collisions de hachage

Le comportement intrinsèque des tables de hachage soulève un aspect pertinent de ces structures de données: même avec un algorithme de hachage efficace, deux objets ou plus peuvent avoir le même code de hachage, même s’ils sont inégaux. Ainsi, leurs codes de hachage pointeraient vers le même compartiment, même s'ils auraient des clés de table de hachage différentes.

Cette situation est communément connue sous le nom de collision de hachage etvarious methodologies exist for handling it, chacun ayant ses avantages et ses inconvénients. JavaHashMap utilisethe separate chaining method pour gérer les collisions:

"Lorsque deux objets ou plus pointent vers le même bucket, ils sont simplement stockés dans une liste liée. Dans un tel cas, la table de hachage est un tableau de listes liées et chaque objet avec le même hachage est ajouté à la liste liée à l'index du compartiment dans le tableau.

In the worst case, several buckets would have a linked list bound to it, and the retrieval of an object in the list would be performed linearly. »

Les méthodologies de collision de hachage montrent en quelques mots pourquoi il est si important d'implémenter efficacementhashCode().

Java 8 a apporté unenhancement to HashMap implementation intéressant - si la taille d'un bucket dépasse le certain seuil, la liste chaînée est remplacée par une carte arborescente. Cela permet d'obtenirO(logn _) _ rechercher au lieu deO(n) pessimistes.

8. Créer une application triviale

Pour tester la fonctionnalité d'une implémentation standard dehashCode(), créons une application Java simple qui ajoute des objetsUser à unHashMap et utiliseSLF4J pour enregistrer un message dans la console chaque fois que la méthode est appelée.

Voici le point d'entrée de l'exemple d'application:

public class Application {

    public static void main(String[] args) {
        Map users = new HashMap<>();
        User user1 = new User(1L, "John", "[email protected]");
        User user2 = new User(2L, "Jennifer", "[email protected]");
        User user3 = new User(3L, "Mary", "[email protected]");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

Et voici l'implémentation dehashCode():

public class User {

    // ...

    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (int) id;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        hash = 31 * hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

Le seul détail qui mérite d'être souligné ici est que chaque fois qu'un objet est stocké dans la carte de hachage et vérifié avec la méthodecontainsKey(),hashCode() est appelé et le code de hachage calculé est imprimé sur la console:

[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection

9. Conclusion

Il est clair que produire des implémentations efficaces dehashCode() nécessite souvent un mélange de quelques concepts mathématiques, (c.-à-d. nombres premiers et arbitraires), opérations mathématiques élémentaires et logiques.

Quoi qu'il en soit, il est tout à fait possible d'implémenter efficacementhashCode() sans recourir du tout à ces techniques, du moment que nous nous assurons que l'algorithme de hachage produit des codes de hachage différents pour des objets inégaux et est cohérent avec l'implémentation deequals().

Comme toujours, tous les exemples de code présentés dans cet article sont disponiblesover on GitHub.