logo le blog invivoo blanc

Réussir mon entretien technique Java : Equals & Hashcode

16 septembre 2021 | Design & Code, Java | 0 comments

Introduction

Cet article, s’inscrivant dans la suite de la thématique « Réussir mon entretien technique en Java », traite de la redéfinition des méthodes « Equals » et « Hashcode » et de son impact sur la performance d’une application en Java.

En effet, la gestion d’égalité d’objets peut facilement devenir un sujet de performance et d’intégrité dans la mesure où elle peut impacter certaines collections de données telles que les « Set » et les « Map ».

Afin de mieux aborder le sujet, nous allons répondre aux questions suivantes :

  • Comment tester l’égalité en Java : différence entre « == » et « Equals » ?
  • Comment redéfinir la méthode « Equals » ?
  • Qu’est-ce que le « Hashcode » ?
  • Quel contrat lie les méthodes « Equals » et « Hashcode » ?
  • Quel impact peut avoir la redéfinition de ces méthodes sur les « Set » et les « Map » ?

Enfin, nous verrons, en annexe, quelques API utiles pour la gestion automatisée des méthodes « Equals » et « Hashcode ».

Si vous désirez vous faire accompagner par des experts, contactez notre page dédiée à la programmation Java.

1. Comment tester l’égalité en Java ?

En Java, la manière de tester l’égalité entre deux variables dépend de leur type :

  • Avec « == » pour les variables de type primitif : « byte, short, int, long, float, double, char »
  • Avec la méthode « equals() » pour les variables de type objet.

En effet, en mémoire, les variables de type primitif pointent directement vers une case qui contient la valeur qui leur est assignée alors que les variables de type Objet, quant-à-elles, pointent vers une case qui contient l’adresse (référence) vers la valeur de l’objet affecté. Par conséquent, les « == » servent à comparer les valeurs pour les types primitifs et les références (adresses) pour les variables de type Objet. Pour rappel, un Objet peut être défini comme étant un ensemble d’informations (attributs, propriétés…) et de comportements (méthodes). De ce fait, comparer deux Objets, reviendrait à comparer les informations contenues, à savoir les attributs caractéristiques d’où l’utilisation de la méthode « equals() ».

2. Comment redéfinir de la méthode « Equals » ?

La méthode « equals() » héritée de la classe « Object » prend en paramètre une variable de type « Object » et retourne un booléen correspondant au résultat de la comparaison.

Pour implémenter un exemple de méthode « equals() », considérons une classe « Person » ayant comme attributs le nom, le prénom et la matricule de la personne. Dans ce cas, comparer deux personnes revient à comparer leurs noms, prénoms et matricules.

NB : Ne pas oublier le cas d’une valeur nulle et de vérifier les références pour éviter une perte de temps :

package com.invivoo.mse.entities;
public class Person {
	private String nom ;
	private String prenom ;
	private String matricule ;
	public Person(String nom, String prenom, String matricule) {
		this.nom = nom ;
		this.prenom = prenom ;
		this.matricule = matricule ;
	}
	public String getNom() {
		return nom;
	}
	public void setNom(String nom) {
		this.nom = nom;
	}
	public String getPrenom() {
		return prenom;
	}
	public void setPrenom(String prenom) {
		this.prenom = prenom;
	}
	public String getMatricule() {
		return matricule;
	}
	public void setMatricule(String matricule) {
		this.matricule = matricule;
	}
@Override
	public boolean equals(Object obj) {
		// Objet nul ?
		if(obj == null) return false ;
		// Même référence ?
		if(this == obj) return true ;
		// Même classe ?
		if(getClass() != obj.getClass()) return false ;
		Person p = (Person) obj ;
		if(nom == null) {
			if(p.nom != null)
				return false ;
		} else if (!nom.equals(p.nom))
			return false ;
		if(prenom == null) {
			if(p.prenom != null)
				return false ;
		} else if(!prenom.equals(p.prenom))
			return false ;
		if(matricule == null) {
			if(p.matricule != null)
				return false ;
		} else if(!matricule.equals(p.matricule))
			return false ;
		return true;
	}
}

Quelques règles sont à respecter lors de la redéfinition de la méthode « equals() » :

  • Réflexivité : pour tout objet a, a.equals(a) est vrai « Tout objet est égal à lui-même »
  • Symétrie : Pour tous objets a et b, si a.equals(b) est vrai alors b.equals(a) doit être vrai « Si a est égal à b alors b aussi est égal à a »
  • Transitivité : Pour tous objets a, b et c : si a.equals(b) est vrai et b.equals(c) vrai alors a.equals(c) doit être vrai
  • Enfin, si une classe redéfinit la méthode « equals() » alors elle doit aussi redéfinir la méthode « hashCode() ».
    • Consistance : Pour tous objets a et b, si a.equals(b) alors a et b doivent avoir le même HashCode.
package com.invivoo.mse;

import com.invivoo.mse.entities.Person;

public class MainEquals {
	
	public static void main(String[] args) {
		
		Person p1 = new Person("toto", "tata", "x42") ;
		Person p2 = new Person("toto", "tata", "x42") ;
		
		System.out.println("Test avec '==' p1 X p2: " + (p1 == p2));
		System.out.println("Test avec 'equals' p1 X p1 : " + p1.equals(p1));
		System.out.println("Test avec 'equals' p1 X p2 : " + p1.equals(p2));
		System.out.println("Test avec 'equals' p2 X p1 : " + p2.equals(p1));
	}
}
Console :
Test avec '==' p1 X p2: false
Test avec 'equals' p1 X p1 : true
Test avec 'equals' p1 X p2 : true
Test avec 'equals' p2 X p1 : true

3. Qu’est-ce que le « HashCode » en Java ?

En Java, le « hashCode » correspond à la valeur de hachage de l’objet en question et sert à retrouver rapidement une instance d’une classe dans un ensemble.

Sans cette approche, retrouver un objet dans un ensemble reviendrait à parcourir les éléments de cet ensemble et faire appel à la méthode « equals()» à chaque itération. Ce qui peut facilement être coûteux avec une complexité temporelle proportionnelle à la cardinalité de l’ensemble étudié. En effet, l’utilisation d’un tableau de hachage réduit la recherche sur un sous-ensemble, ce qui réduit également la complexité.

La méthode « hashCode()» ne prend aucun paramètre et retourne un entier. Par conséquent il est clair que cet entier ne représente pas l’identifiant unique de l’instance car cela limiterait le nombre d’instances possibles d’une classe.

L’entier retourné est le résultat d’un calcul effectué sur les propriétés de l’objet sur lequel la méthode est invoquée. De préférence, ces attributs doivent exactement correspondre à ceux qui sont utilisés dans la méthode « equals()». Des instances ayant des valeurs différentes peuvent avoir le même hashCode. Cependant si des instances ont les mêmes valeurs alors elles doivent forcément retourner le même hashCode.

NB :

  • Si deux objets sont égaux alors ils ont le même hashCode.
  • Mais si deux objets ont le même hashCode, ils ne sont pas forcément égaux car le hashCode ne garantit pas l’unicité.
  • De préférence, le hashCode doit se calculer sur les mêmes attributs comparés dans la méthode « equals() ».
  • Exemple :
package com.invivoo.mse;
	
import com.invivoo.mse.entities.Person;

public class Person {
	...
	
	@Override
	public int hashCode() {
		int res = 1 ; 
		int cote = 42 ;
		res = res*cote + (nom != null ? nom.hashCode() : 0) ; 
		res = res*cote + (prenom != null ? prenom.hashCode() : 0) ; 
		res = res*cote + (matricule != null ? matricule.hashCode() : 0) ; 
		return res ;
	}
}

4. Quel est l’impact sur les « Map » et « Set » ?

Pour rappel, les Map correspondent à une collection de données organisée en paires de clé-valeur. Elles disposent d’une table de hachage ayant plusieurs compartiments identifiés, chacun, par le code de hachage des clés des paires contenues.

A l’insertion d’une paire clé-valeur, la Map calcule l’identificateur du compartiment « bucket » correspondant et y stocke la paire. A la recherche par une clé donnée, la Map regarde directement dans le compartiment correspondant (haschode de la clé) et compare les clés trouvées à celle fournie en faisant appel à la méthode « equals() ».

Les Set appartiennent également à la famille des collections basées sur du hachage avec un caractère particulier d’unicité des valeurs. Ils utilisent, par conséquent, la méthode « hashCode » pour déterminer la clé du compartiment de chaque objet inséré et la méthode « equals » pour garantir l’unicité des objets.

Par conséquent, une mal implémentation des méthodes « equals » et « hashCode » pourraient sévèrement affecter le fonctionnement des Map et des Set :

  • Exemples :
    • Si la méthode « hashCode » d’un objet utilisé en tant que clé dans un Map retourne toujours :
      • Une valeur constante : cela affecterait la performance de cette Map en faisant passer la complexité de l’insertion, suppression et recherche de O(1) à O(n).
      • Une valeur aléatoire : cela rendrait la Map incohérente et le contrat d’implémentation ne serait plus respecté. En effet, rien ne garantirait le résultat de la recherche d’une valeur précédemment insérée.
    • Si la méthode « equals » est fausse :
      • Pour les Map, impossible de retrouver une paire stockée : l’intégrité ne sera plus garantie.

Pour les Set, le contrat de l’unicité ne sera plus respecté.

5. Guava : redéfinition des méthodes equals/hashcode en Java

En Java, il existe quelques librairies facilitant la redéfinition des méthodes « equals » et « hashCode » qui peut s’avérer longue et demandant beaucoup de précaution. Parmi ces librairies, nous pouvons citer :  Guava, Lombok, Apache Commons…

Exemple de redéfinition de la méthode « equals » avec Guava :

  ...

    import com.google.common.base.Objects; 

    ...

    @Override
    public boolean equals(Object other) {
        return other != null
                && getClass() == other.getClass()
                && Objects.equal(nom, ((Person) other).nom)
                && Objects.equal(prenom, ((Person) other).prenom)
                && Objects.equal(matricule, ((Person) other).matricule);
    }

Exemple de redéfinition de la méthode « hashcode » avec Guava :

    ...

    import com.google.common.base.Objects; 

    ...

    @Override
    public int hashCode() {
        return Objects.hashCode(nom, prenom, matricule);
    }

Conclusion

En résumé, il est important de savoir que la manière de comparer des variables en Java dépend de leur type :

  • « == » pour les types primitifs
  • « equals » pour les types référencés (Objet)

Pour une entité donnée, il est vivement conseillé de redéfinir sa méthode « equals » pour décrire la situation d’égalité et par conséquent la méthode « hashCode » pour garantir le respect du contrat qui lie ces deux méthodes.

L’implémentation de la méthode « equals » doit respecter la réflexivité, la symétrie, la transitivité, la consistance et doit prendre en compte les valeurs nulles. Cependant, il faut savoir qu’une mal redéfinition de ces méthodes pourraient sévèrement impacter l’intégrité, la performance et la cohérence des collections utilisant des tables de hachage (Map, Set…).

Face la complexité de la redéfinition des méthodes « equals » et « hashcode », il existe quelques librairies facilitant l’écriture de manière précautionneuse :