Developpez.com - Java
X

Choisissez d'abord la catégorieensuite la rubrique :

 

Développons en Java   2.10  
Copyright (C) 1999-2016 Jean-Michel DOUDOUX    (date de publication : 19/03/2016)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

97. Les techniques de développement spécifiques à Java

 

chapitre 9 7

 

Niveau : niveau 4 Supérieur 

 

Le développement en Java requiert la mise en oeuvre de quelques techniques particulières dédiées à ce langage.

Ce chapitre couvre des techniques de développement spécifiques à Java. Ces techniques ne concernent que Java et plus particulièrement certaines de ses particularités.

Ce chapitre contient plusieurs sections :

 

97.1. L'écriture d'une classe dont les instances seront immuables

Un objet immuable est un objet dont on ne peut plus modifier l'état une fois l'instance créée.

Pour rendre un objet immuable, il faut respecter plusieurs consignes lors de l'écriture de sa classe :

Les objets immuables possèdent plusieurs avantages :

Toutes les classes de type wrapper du package java.lang sont immuables : Boolean, Byte, Character, Double, Float, Integer, Long et Short.

La classe String est sûrement la classe immuable la plus connue et la plus utilisée.

Exemple :
package com.jmdoudoux.test;

public class TestString {
  public static void main(String[] args) {
    String chaine = new String("Bonjour");
    System.out.println(chaine);
    chaine.replaceAll("jour", "soir");
    System.out.println(chaine);
  }
}
Résultat :
Bonjour
Bonjour

Il est cependant parfois nécessaire d'avoir une classe immuable et la même classe modifiable : l'exemple le plus connu est la classe String et les classes StringBuffer et StringBuilder.

Il est généralement recommandé d'utiliser des objets immuables le plus souvent possible.

Il est très important que les objets renvoyés par les getters ne puissent pas être modifiés sinon la classe n'est plus immuable.

Exemple :
package com.jmdoudoux.test;

import java.util.Date;

public final class Personne {
  private final String nom;
  private final String prenom;
  private final Date dateNaiss;
  
  public Personne(String nom, String prenom, Date dateNaiss) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.dateNaiss = dateNaiss;
  }
  
  public String getNom() {
    return nom;
  }
  
  public String getPrenom() {
    return prenom;
  }
  
  public Date getDateNaiss() {
    return dateNaiss;
  }
  
  @Override
  public String toString() {
    StringBuilder result = new StringBuilder("nom=");
    result.append(nom);
    result.append(", prenom=");
    result.append(prenom);
    result.append(", dateNaiss=");
    result.append(dateNaiss);
    return result.toString();
  }
}

Le code de cette classe peut laisser à penser que les objets de cette classe seront immuables : ce n'est pas le cas.

Exemple :
package com.jmdoudoux.test;
      
import java.util.Date;

public class TestImmuable {

  public static void main(String[] args) {
    Date dateNaiss = new Date();
    Personne personne = new Personne("nom1", "prenom1", dateNaiss);
    System.out.println(personne);
    Date nouvelleDateNaiss = personne.getDateNaiss();
    nouvelleDateNaiss.setMonth(nouvelleDateNaiss.getMonth() + 1);
    System.out.println(personne);
  }
}
Résultat :
nom=nom1,prenom=prenom1, dateNaiss=Sat Dec 03 20:58:49 CET 2011
nom=nom1,prenom=prenom1, dateNaiss=Tue Jan 03 20:58:49 CET 2012

La classe n'est pas immuable puisqu'il a été possible de modifier une de ses propriétés et comme cet objet n'est pas immuable, ses propriétés sont modifiables.

Il est donc nécessaire que les getters renvoient une instance immuable ou une autre instance de la classe qui encapsule les mêmes propriétés.

Exemple :
package com.jmdoudoux.test;
      
import java.util.Date;

public final class Personne {
  private final String nom;
  private final String prenom;
  private final Date dateNaiss;
  
  // ...

  public Date getDateNaiss() {
    return new Date(dateNaiss.getTime());
  }
  
  // ...

}

Si l'on exécute de nouveau la classe de test, l'objet reste inchangé.

Résultat :
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 20:58:49 CET 2011
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 20:58:49 CET 2011

Malgré cette modification, l'objet n'est toujours pas immuable.

Exemple :
package com.jmdoudoux.test;

import java.util.Date;

public class TestImmuable {
  
  public static void main(String[] args) {
    Date dateNaiss = new Date();
    Personne personne = new Personne("nom", "prenom", dateNaiss);
    System.out.println(personne);
    dateNaiss.setMonth(dateNaiss.getMonth() + 1);
    System.out.println(personne);
  }
}

Il suffit qu'une référence sur l'objet passée en paramètre lors de la création de l'objet soit conservée pour que l'objet ne soit toujours pas immuable.

Résultat :
nom=nom, prenom=prenom, dateNaiss=Sat Dec 03 21:09:26 CET 2011
nom=nom, prenom=prenom, dateNaiss=Tue Jan 03 21:09:26 CET 2012

Si un objet fourni en paramètre du constructeur n'est pas immuable, alors il est nécessaire d'en conserver une copie profonde (deep copy), un clone ou une version immuable.

Exemple :
package com.jmdoudoux.test;
      
import java.util.Date;

public final class Personne {
  private final String nom;
  private final String prenom;
  private final Date dateNaiss;
  
  public Personne(String nom, String prenom, Date dateNaiss) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.dateNaiss = new Date(dateNaiss.getTime());
  }
  
  // ...

  
  public Date getDateNaiss() {
    return new Date(dateNaiss.getTime());
  }
  
  // ...

}

Si l'on exécute de nouveau la classe de test, l'objet reste inchangé et il est bien immuable..

Résultat :
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 20:58:49 CET 2011
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 20:58:49 CET 2011

Dans tous les cas en Java, il est possible de passer outre les mécanismes de protection standard utilisés pour garantir l'immutabilité des objets d'une classe en utilisant l'introspection.

Exemple :
package com.jmdoudoux.test;
      
import java.lang.reflect.Field;

public class MaClasse {
  public static void modifierChaine(String chaine, String valeur) {
    try {
      Field stringValue = String.class.getDeclaredField("value");
      stringValue.setAccessible(true);
      stringValue.set(chaine, valeur.toCharArray());
    } catch (Exception ex) {
    }
  }
}

L'utilisation de l'introspection permet de modifier n'importe quelle valeur d'une propriété private, comme pour une chaîne de caractères dans l'exemple ci-dessus.

Exemple :
package com.jmdoudoux.test;

import java.util.Date;

public class TestImmuable {
  public static void main(String[] args) {
    Personne personne = new Personne("nom1", "prenom1", new Date());
    System.out.println(personne);
    MaClasse.modifierChaine(personne.getNom(), "nom2");
    System.out.println(personne);
  }
}
Résultat :
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 22:32:27 CET 2011
nom=nom2, prenom=prenom1, dateNaiss=Sat Dec 03 22:32:27 CET 2011

Pour empêcher l'utilisation de l'introspection, il faut utiliser un gestionnaire de sécurité, qui par défaut va limiter l'accès aux membres d'une classe.

Par exemple en ajoutant l'option -Djava.security.manager à la JVM, une exception de type AccessControlException va être levée lors de la tentative d'accès à un membre d'un objet par introspection.

Résultat :
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 23:14:41 CET 2011
nom=nom1, prenom=prenom1, dateNaiss=Sat Dec 03 23:14:41 CET 2011
java.security.AccessControlException:
access denied (java.lang.RuntimePermission accessDeclaredMembers)
      at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
      at java.security.AccessController.checkPermission(AccessController.java:546)
      at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
      at java.lang.SecurityManager.checkMemberAccess(SecurityManager.java:1662)
      at java.lang.Class.checkMemberAccess(Class.java:2157)
      at java.lang.Class.getDeclaredField(Class.java:1879)
      at com.jmdoudoux.test.MaClasse.modifierChaine(MaClasse.java:9)
      at com.jmdoudoux.test.TestImmuable.main(TestImmuable.java:10)

 

97.2. La redéfinition des méthodes equals() et hashCode()

La classe Object possède deux méthodes qui sont relatives à l'identité des objets : equals() et hashCode().

La méthode equals() permet de tester l'égalité de deux objets d'un point de vue sémantique.

La méthode hashCode() permet de renvoyer la valeur de hachage de l'objet sur lequel elle est invoquée.

Les spécifications imposent une règle à respecter lors de la redéfinition de ces méthodes : si une classe redéfinit la méthode equals() alors elle doit aussi redéfinir la méthode hashCode() et inversement. Le comportement de ces deux méthodes doit être symétrique : si les méthodes hashCode() et equals() sont redéfinies alors elles doivent utiliser, de préférence, toutes les deux les même champs car deux objets qui sont égaux en utilisant la méthode equals() doivent obligatoirement avoir tous les deux la même valeur de retour lors de l'invocation de leur méthode hashCode(). L'inverse n'est pas forcément vrai.

Le hashcode ne fournit pas un identifiant unique pour un objet : de toute façon le hashcode d'un objet est de type int, ce qui limiterait le nombre d'instances possibles d'une classe.

Deux objets pouvant avoir le même hashcode, il faut alors utiliser la méthode equals() pour déterminer s'ils sont identiques.

 

97.2.1. Les contraintes pour redéfinir equals() et hashCode()

Les méthodes equals() et hashCode() sont étroitement liées.

La redéfinition des méthodes equals() et hashcode() doit respecter quelques contraintes qui sont précisées dans la documentation de la classe Object :

Aucune spécification n'est imposée concernant l'implémentation des méthodes equals() et hashCode() pour leur permettre d'être consistantes.

Cependant, l'implémentation de la méthode hashcode() doit être consistante avec la méthode equals() : si la méthode equals() renvoie true pour deux objets alors la méthode hashCode() invoquée sur les deux objets doit renvoyer la même valeur. L'inverse n'est pas vrai, deux objets dont la méthode hashCode() renvoie la même valeur, n'implique pas obligatoirement que l'invocation de la méthode equals() sur les deux objets renvoie true.

Il est donc nécessaire de redéfinir les méthodes hashCode() et equals() de manière coordonnée si l'une ou l'autre est redéfinie. Pour garantir le contrat entre les méthodes equals() et hashCode() et leur efficacité maximale, il est préférable que leur implémentation utilise les mêmes champs de la classe.

Pour les classes qui implémentent l'interface Comparable, il est aussi important de maintenir une cohérence entre les méthodes equals() / hashCode() et la méthode compareTo(). Les spécifications précisent que si la méthode compareTo() renvoie 0 alors la méthode equals() doit renvoyer true et inversement. Cela implique aussi que si equals() renvoie false, alors la méthode compareTo() doit renvoyer une valeur différente de 0.

 

97.2.2. La méthode equals()

L'opérateur == vérifie si deux objets sont identiques : il compare que les deux objets possèdent la même référence mémoire et sont donc en fait le même objet.

Deux objets identiques sont égaux mais deux objets égaux ne sont pas forcement identiques.

La méthode equals() vérifie l'égalité de deux objets : son rôle est de vérifier si deux instances sont sémantiquement équivalentes même si ce sont deux instances distinctes.

Chaque classe peut avoir sa propre implémentation de l'égalité mais généralement deux objets sont égaux si tout ou partie de leurs états sont égaux.

Exemple :
public class TestEquals {
  
  public static void main(String[] args) {
    String chaine1 = new String("test");
    String chaine2 = new String("test");
    boolean isSame = (chaine1 == chaine2);
    System.out.println(isSame);
    boolean isEqual = (chaine1.equals(chaine2));
    System.out.println(isEqual);
  }
}
Résultat :
false
true

Deux mêmes objets sont égaux s'ils possèdent la même référence évidement mais deux objets distincts peuvent aussi être égaux si l'invocation de la méthode equals() du premier avec le second en paramètre renvoie true.

 

97.2.2.1. L'implémentation par défaut de la méthode equals()

L'implémentation par défaut de la méthode equals() dans la classe Object est la suivante :

Exemple :
public boolean equals(Object obj) {
  return (this == obj);
}

Par défaut, l'implémentation de la méthode equals() héritée de la classe Object teste donc l'égalité de l'adresse mémoire des objets.

Il y a un contrat à respecter entre les méthodes equals() et hashCode() : comme précisé dans la javadoc, si l'invocation de la méthode equals() avec deux instances renvoie true alors l'invocation de la méthode hashCode() de ces deux instances doit renvoyer la même valeur. Cette implémentation respecte ce contrat.

Cette implémentation par défaut de la méthode equals(), héritée de la classe Object, a le mérite de fonctionner pour tous les objets mais son mode de fonctionnement n'est pas toujours souhaité pour tous les objets.

Exemple :
import java.util.Date;
      
public class Personne {
 
  private String nom;
  private String prenom;
  private long id;
  private Date dateNaiss;
  private boolean adulte;
 
  public Personne(String nom, String prenom, long id, Date dateNaiss,
    boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }  
}

Si l'on crée deux instances de cette classe avec les même paramètres et que l'on teste l'égalité sur les deux instances, le résultat est false puisque ce sont deux instances distinctes.

Exemple :
public class TestEqualsPersonne {
 
  public static void main(String[] args) {
    Personne p1 = new Personne("nom1", "prenom1", 1, null, true);
    Personne p2 = new Personne("nom1", "prenom1", 1, null, true);
    System.out.println(p1.equals(p2)); 
  }
}
Résultat :
false

Logiquement, on pourrait espérer que ce test renvoie true mais pour cela il faut redéfinir la méthode equals().

 

97.2.2.2. La redéfinition de la méthode equals()

La redéfinition de la méthode equals est un besoin fréquent mais il n'est pas toujours facile d'écrire une implémentation correcte qui tienne compte de la sémantique de la classe.

La méthode equals() permet de vérifier si l'objet qui lui est fourni en paramètre est égal à l'objet sur lequel la méthode est invoquée. Sa signature est la suivante :
public boolean equals(Object obj)

L'implémentation par défaut de cette méthode héritée de la classe Object vérifie simplement si les références des deux objets sont les mêmes (this == obj). Comme la classe Object ne possède pas de champs, c'est le seul test qu'elle peut réaliser pour tester l'égalité.

La redéfinition de la méthode equals() permet de fournir des règles particulières pour le test d'égalité des objets d'une classe. Dans ce cas, son implémentation utilise généralement un test d'égalité reposant sur tout ou partie des champs de la classe qui sont pertinents pour sa discrimination.

L'implémentation de la méthode equals() est à la charge du développeur qui doit définir ce qu'est l'égalité entre deux objets de cette classe. La classe Object propose une implémentation par défaut qui test simplement l'égalité sur les références des deux objets. Comme toutes les classes héritent de la classe Object, si leur méthode equals() n'est pas redéfinie, alors deux objet sont égaux si et seulement si ces objets ont les mêmes références.

Il est donc généralement nécessaire de redéfinir la méthode equals() pour lui donner un rôle sémantique par rapport aux champs de la classe. Par exemple :

Il est préférable d'utiliser les champs qui concernent l'état de l'objet : ceci implique généralement de ne pas prendre en compte les champs static et les champs transient.

L'implémentation de la méthode equals() n'est pas toujours facile et dépend de la classe. Si la classe est immuable alors l'implémentation de la méthode equals() peut utiliser la comparaison de l'état de l'objet avec l'état de l'objet fourni en paramètre.

L'implémentation de la méthode equals() pour une classe qui n'est pas immuable est plus difficile car il faut décider si l'égalité va se faire sur tout ou partie de l'état de l'objet ou sur l'identité de l'objet (l'implémentation de la classe Object utilise la référence par exemple). Ce choix dépend de l'utilisation qui sera faite des instances de la classe.

L'implémentation de la méthode equals() peut parfois être complexe selon les besoins. Par exemple, la méthode equals() de l'interface List vérifie que l'autre objet est aussi de type List, que les deux collections possèdent le même nombre d'éléments, qu'ils contiennent les mêmes éléments en utilisant leur méthode equals() et que ces éléments sont dans le même ordre.

 

97.2.2.3. Les contraintes et quelques recommandations

L'implémentation d'une redéfinition de la méthode equals() doit respecter plusieurs caractéristiques :

Le non respect des règles qui définissent le contrat de la méthode equals() peut induire des bugs difficiles à identifier car ce sont des problèmes de conception.

Il n'existe pas de solution unique pour redéfinir la méthode equals() tant que les contraintes imposées par la spécification sont respectées. La plupart des IDE propose même une fonctionnalité pour générer cette méthode à partir de tout ou partie des champs de la classe.

Exemple :
import java.util.Date;

public class Personne {
 
  private String nom;
  private String prenom;
  private long id;
  private Date dateNaiss;
  private boolean adulte;
 
  public Personne(String nom, String prenom, long id, Date dateNaiss,
    boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
     return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Personne other = (Personne) obj;
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
        return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
        return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }
  // ...  

}

Lors de la redéfinition de la méthode equals(), il faut bien faire attention à respecter la signature de la méthode qui attend en paramètre une instance de type Object sinon c'est une surcharge qui se compilera sans soucis mais qui ne sera pas invoquée pour tester l'égalité.

Pour éviter ce problème, il faut utiliser l'annotation @Override sur la redéfinition de la méthode, assurant ainsi une erreur à la compilation si la méthode n'est pas une redéfinition d'une méthode héritée.

La redéfinition de la méthode equals() n'est pas obligatoire et n'est pas toujours forcement nécessaire notamment :

Si le test de l'égalité d'une classe n'a pas de sens, il est préférable de redéfinir la méthode equals() pour qu'elle lève une exception de type UnsupportedOperationException. Ceci permet d'éviter d'avoir le comportement d'une des classes mère, qui peut être celui de la classe Object.

La méthode equals() est fréquemment utilisée notamment dans la plupart des implémentations de collections pour savoir si un objet est déjà présent dans la collection ou non. Il est donc important que la redéfinition de la méthode equals() soit optimisée et efficace surtout si le nombre d'instances dans la collection est important.

Pour optimiser ces performances, il est par exemple possible de suivre quelques recommandations :

Par contrat, la méthode equals() attend un objet de type Object : il est donc préférable avant de tester l'égalité des membres de la classe de s'assurer de l'égalité du type de la classe avec celui de celle fournie en paramètre.

Il y a deux manières de vérifier l'égalité de la classe avant de vérifier l'égalité des membres :

Chacune de ces solutions a son utilité selon les circonstances et l'utilisation de l'une ou l'autre dépend des besoins.

Il est généralement préférable de tester que les objets soient du même type en testant l'égalité de l'invocation de leur méthode getClass(). Ce test permet de renvoyer false si l'instance fournie en paramètre est une sous-classe de l'instance courante. Ce type de test n'est pas obligatoire mais dans ce cas, les classes qui peuvent être passées en paramètre de la méthode equals() doivent faire de même pour respecter la règle de symétrie et de réflexivité.

Cependant, pour certains cas particuliers, il peut être souhaitable de tester que les objets soient du même type en utilisant l'opérateur instanceof. Un exemple de cas particulier concerne les entités utilisées avec Hibernate : comme ce dernier peut créer des proxys, il est préférable d'utiliser l'opérateur instanceof.

Attention cependant, ce n'est généralement pas une bonne idée d'utiliser l'opérateur instanceof lorsque la méthode equals() doit être redéfinie car généralement cela peut violer la règle de symétrie que doit respecter l'implémentation de la méthode equals().

Le test sur l'égalité des classes des deux instances permet de pouvoir étendre la classe sans avoir à redéfinir la méthode equals() pour respecter la règle concernant la symétrie.

 

97.2.3. La méthode hashCode()

La méthode hashCode() retourne valeur de hachage calculée sur l'instance d'un objet.

La valeur du hash code est essentiellement utilisée par les collections de type Hashxxx (java.util.Hashtable, java.util.HashMap, java.util.HashSet et leurs sous-classes, ...) qui utilisent la valeur de hachage pour améliorer leur performance.

La valeur de hachage peut également être utilisée dans un autre contexte que celui des collections : par exemple, pour améliorer les performances en Java SE 7, le compilateur transforme les instructions switch utilisant des chaînes de caractères en une série d'instructions if qui testent d'abord la valeur de hash.

La définition de la méthode hashCode() dans la classe Object possède la signature suivante :

Exemple :
public native int hashCode();

Cette méthode est déclarée native car c'est l'implémentation de la JVM qui peut obtenir l'adresse mémoire de l'objet. Par défaut, la méthode hashCode(), définie dans la classe Object, utilise l'adresse mémoire de l'instance pour créer la valeur de type int du hashcode de l'instance.

Il est cependant possible de redéfinir cette méthode puisque toutes les classes héritent de la classe Object.

 

97.2.3.1. L'implémentation par défaut

La classe Object propose une implémentation par défaut de la méthode hashCode() qui renvoie la référence de l'objet sous la forme d'une valeur de type int. Il est possible sur certaines plate-formes que la valeur de la référence soit supérieure à la capacité d'un entier de type int : c'est pour cette raison que deux objets distincts peuvent avoir le même hashcode.

Si la méthode hashCode() est redéfinie, il est possible d'obtenir la valeur du hashcode par défaut telle qu'elle serait renvoyée par l'implémentation de la méthode hashCode() fournie par la classe Object en utilisant la méthode System.identityHashCode().

Exemple :
public class TestHashcode {
 
  public static void main(String[] args) {
    String chaine = "ma chaine";
    System.out.println("chaine.hashCode() = " + chaine.hashCode());
    int identityHashcode = System.identityHashCode(chaine);
    System.out.println("chaine identityHashcode = " + identityHashcode);
    Object monObjet = new Object();
    System.out.println("monObjet.hashCode() = " + monObjet.hashCode());
    identityHashcode = System.identityHashCode(monObjet);
    System.out.println("monObjet identityHashcode = " + identityHashcode);
  }
}
Résultat :
chaine.hashCode() = -921457200
chaine identityHashcode = 4072869
monObjet.hashCode() = 1671711
monObjet identityHashcode = 1671711

 

97.2.3.2. La redéfinition de la méthode hashCode()

Comme précisé pour la méthode equals() de la classe Object dans la Javadoc, il est nécessaire de redéfinir la méthode hashCode() si la méthode equals() est redéfinie car il faut respecter le contrat qui précise que deux objets égaux doivent avoir le même hashcode.

Généralement, la redéfinition de la méthode equals() utilise tout ou partie des attributs de la classe pour tester l'égalité de deux objets. Il est généralement pratique d'utiliser les mêmes attributs dans le calcul du hashcode afin de garantir que deux objets égaux ont le même hashcode.

La problématique est que la valeur de retour de la méthode hashCode() est de type int : il est donc nécessaire d'appliquer un algorithme qui va déterminer une valeur de type int à partir des champs à utiliser. Il est nécessaire que cet algorithme assure que la valeur de hachage calculée soit toujours la même avec les mêmes attributs. Généralement, cet algorithme calcule une valeur de type int pour chaque attributs et combine ces valeurs en utilisant un multiplicateur (généralement un nombre premier) pour déterminer la valeur de hachage.

Il n'existe pas de solution unique pour redéfinir la méthode hashCode() tant que les contraintes imposées par la spécification sont respectées.

Exemple :
import java.util.Date;
      
public class Personne {
 
  private String nom;
  private String prenom;
  private long id;
  private Date dateNaiss;
  private boolean adulte;
 
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (adulte ? 1231 : 1237);
    result = prime * result + ((dateNaiss == null) ? 0 : dateNaiss.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((nom == null) ? 0 : nom.hashCode());
    result = prime * result + ((prenom == null) ? 0 : prenom.hashCode());
    return result;
  }  
}

 

97.2.3.3. Les contraintes et les recommandations

La redéfinition de la méthode hashCode() doit explicitement respecter plusieurs règles :

L'implémentation par défaut de la méthode hashCode(), héritée de la classe Object et qui utilise la référence de l'objet, respecte ces règles :

Deux objets égaux doivent avoir la même valeur de hachage tant qu'ils restent égaux mais deux objets non égaux n'ont pas l'obligation d'avoir des valeurs de hachage distinctes. Pour respecter ces deux règles, il est nécessaire de redéfinir la méthode hashCode() lorsque la méthode equals() est redéfinie.

De plus, par défaut, la méthode hashCode() renvoie la valeur de type int déterminée à partir de l'adresse mémoire de l'instance. Cela permet d'avoir une bonne répartition des valeurs retournées par la méthode hashCode() mais ne permet pas de retourner la même valeur pour deux instances dont la méthode equals() est redéfinie pour tester l'égalité des valeurs de leur propriété. Il faut donc redéfinir la méthode hashCode() en conséquence si la méthode equals() est redéfinie et assurer ainsi une cohérence entre leurs implémentations.

La façon la plus simple de garantir que deux objets égaux possèdent la même valeur de hachage est d'utiliser les mêmes attributs de la classe dans l'implémentation des méthodes equals() et hashCode().

La redéfinition de la méthode hashCode() doit éviter au maximum de renvoyer la même valeur pour deux instances même si cela est quasi impossible puisque les valeurs possibles sont celles du type int du hashcode et qu'elles doivent être calculées le plus rapidement possible.

Le simple fait que l'implémentation de la méthode hashCode() renvoie une valeur fixe pour toutes les instances est une implémentation qui respecte les règles : deux objets égaux auront forcément le même hashcode et la valeur du hashcode d'un objet sera obligatoirement consistante lors de plusieurs invocations de la méthode hashCode(). Cependant, cette implémentation implique de très mauvaises performances lors de l'utilisation dans des collections de type HashXXX.

La valeur de hachage des différents objets doit être assez significative et représentative dans la plage des valeurs permises par un entier de type int. Pour atteindre cet objectif, quelques règles peuvent être utilisées :

Une implémentation de la méthode hashCode() utilise donc fréquemment un ou deux nombres premiers et une expression mathématique dans l'algorithme de calcul. Généralement, l'algorithme utilise une combinaison des valeurs de hachage des différents attributs qui composent la classe.

Il n'est pas forcement nécessaire d'utiliser tous les attributs mais il faut dans ce cas sélectionner les attributs qui permettront d'être le plus discriminant. Il faut cependant garantir le respect de la règle de cohérence entre la méthode hashCode() et equals().

Les spécifications n'imposent aucun algorithme pour l'implémentation de la méthode hashCode() de la classe Object. Il n'est donc pas possible de se baser sur la valeur de hachage par défaut entre deux JVM de deux fournisseurs.

Il est très important d'optimiser le calcul de la valeur retournée par la méthode hashCode().

Ainsi si le calcul de la valeur du hashcode est complexe ou pour améliorer les performances, il est possible de mettre en cache la valeur de hachage calculée. Deux cas de figure sont à prendre en compte :

Le stockage de la valeur de hachage est donc une solution utilisable sans soucis pour un objet immuable.

Exemple :
import java.util.Date;
      
public class PersonneImmuable {
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;
  private final int cacheHashCode;

  public PersonneImmuable(String nom, String prenom, long id, Date dateNaiss,
      boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
    this.cacheHashCode = calculerHashCode();
  }

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

  private int calculerHashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (adulte ? 1231 : 1237);
    result = prime * result + ((dateNaiss == null) ? 0 : dateNaiss.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((nom == null) ? 0 : nom.hashCode());
    result = prime * result + ((prenom == null) ? 0 : prenom.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;
    PersonneImmuable other = (PersonneImmuable) obj;
    if (hashCode() != other.hashCode()) {
      return false;
    }
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
        return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
        return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }

  // ...

}

La valeur de hachage mise en cache peut être utilisée pour optimiser l'algorithme de la méthode equals() : si la valeur de hachage est différente alors les objets ne sont pas égaux. Attention cependant, l'inverse n'est pas vrai : si les valeurs de hachage sont égales alors les objets ne sont peut être pas égaux.

Pour une classe qui n'est pas immuable, la mise en cache de la valeur de hash est beaucoup moins triviale car la valeur doit être recalculée à chaque fois que la valeur d'un attribut qui entre dans le calcul de la valeur de hachage est modifiée.

La mise en cache de la valeur de hachage n'est peut être pas une bonne idée si le nombre d'instances est très important car cela risque d'occuper beaucoup de place dans le heap.

 

97.2.4. Des exemples de redéfinition des méthodes hashCode() et equals()

Cette section propose plusieurs implémentations des méthodes hashCode() et equals() utilisant des outils pour leur génération ou leur mise en oeuvre.

 

97.2.4.1. L'utilisation d'un IDE

Les IDE fournissent des fonctionnalités pour générer les méthodes hashCode() et equals() à partir de tout ou partie des attributs de la classe. Il ne faut cependant pas oublier de les regénérer si un attribut est ajouté ou retiré à la classe.

L'exemple ci-dessous démontre, pour une classe donnée, une implémentation possible des méthodes equals() et hashCode() générées grâce à l'IDE Eclipse.

Exemple :
import java.util.Date;

public class Personne {
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;

  public Personne(String nom, String prenom, long id, Date dateNaiss,
      boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (adulte ? 1231 : 1237);
    result = prime * result + ((dateNaiss == null) ? 0 : dateNaiss.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((nom == null) ? 0 : nom.hashCode());
    result = prime * result + ((prenom == null) ? 0 : prenom.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;
    Personne other = obj;
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
        return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
        return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }
}

L'implémentation des méthodes equals() et hashCode() utilisent toutes les deux les mêmes attributs de la classe.

L'implémentation de la méthode equals() effectuent plusieurs tests pour vérifier l'égalité de l'instance avec celle fournie en paramètre :

L'implémentation de la méthode hashCode() utilise une formule mathématique reposant sur des nombres premiers et la valeur de hachage des attributs qui sont des objets. Elle utilise les mêmes attributs que ceux qui sont utilisés dans l'implémentation de la méthode equals() de la classe.

 

97.2.4.2. L'utilisation des helpers de Commons Lang

Pour faciliter l'implémentation des méthodes equals() et hashCode(), il est possible d'utiliser respectivement les helpers EqualsBuilder et HashCodeBuilder de la bibliothèque Apache Commons Lang.

Exemple :
import java.util.Date;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Personne {
 
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;
 
  public Personne(String nom, String prenom, long id, Date dateNaiss,
     boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Personne other = (Personne) obj;
    return new EqualsBuilder().append(adulte, other.adulte)
        .append(dateNaiss, other.dateNaiss).append(id, other.id)
        .append(nom, other.nom).append(prenom, other.prenom).isEquals();
  }
 
  @Override
  public int hashCode() {
    return new HashCodeBuilder(17, 31).append(nom).append(prenom).append(id)
        .append(dateNaiss).append(adulte).toHashCode();
  }
}

Pour la méthode equals(), il faut créer une instance de la classe EqualsBuilder : cette classe permet de vérifier l'égalité des champs des deux objets fournis lors de l'invocation de la méthode append().

Si la classe est une classe fille, il faut aussi invoquer la méthode appendSuper() en lui passant en paramètre le résultat de l'invocation de la méthode super.equals().

Le résultat du calcul de la valeur de hachage est obtenu en invoquant la méthode isEquals().

Pour la méthode hashCode(), il faut créer une instance de la classe HashCodeBuilder en passant au constructeur deux nombres premiers choisis aléatoirement.

Il faut invoquer la méthode append() pour chaque champ qui doit entrer dans le calcul de la valeur de hachage en lui passant en paramètre la valeur du champ.

Si la classe est une classe fille, il faut aussi invoquer la méthode appendSuper() en lui passant en paramètre le résultat de l'invocation de la méthode super.hashCode().

Le résultat du calcul de la valeur de hachage est obtenu en invoquant la méthode toHashCode().

 

97.2.5. L'intérêt de redéfinir les méthodes hashCode() et equals()

La méthode hashCode() est essentiellement utilisée par les collections pour optimiser le classement et la recherche de leurs éléments.

Il faut s'assurer que les valeurs de hash des objets qui sont utilisées comme clés dans une Map soient suffisamment diversifiées pour ne pas avoir de problèmes de performances notamment si le nombre d'occurrences dans la collection est important.

Il est aussi très important que la valeur du hashcode ne change pas pour une instance qui est utilisée comme clé dans une collection de type Map.

Pour s'éviter des ennuis difficilement détectables, il faut absolument utiliser des objets immuables comme clés dans une collection de type Map. Le comportement des objets de type Map n'est pas spécifié si un objet utilisé comme clé est modifié en impliquant une modification de la valeur de son hash code.

 

97.2.5.1. L'utilisation par certaines collections

Lors de l'utilisation d'objets dans les collections, il est important de redéfinir de manière adéquate les méthodes equals() et hashCode().

Exemple :
public class Valeur {
 
  private final int valeur;
 
  public Valeur(int valeur) {
    super();
    this.valeur = valeur;
  }
 
  public int getValeur() {
    return valeur;
  }
}

La classe de test insère plusieurs instances d'un objet dans une collection de type HashSet. Une nouvelle instance de la classe avec un attribut identique est recherchée dans la collection et supprimée.

Exemple :
import java.util.HashSet;

public class TestValeur {
 
  public static void main(String[] args) {
    Set<Valeur> hs = new HashSet<Valeur>();
   
    Valeur valeur1 = new Valeur(1);
    Valeur valeur2 = new Valeur(2);
    Valeur valeur3 = new Valeur(3);
   
    hs.add(valeur1);
    hs.add(valeur2);
    hs.add(valeur3);
   
    valeur2 = new Valeur(2);
    System.out.println("hs.size()=" + hs.size());
    System.out.println("hs.contains(valeur2)=" + hs.contains(valeur2));
    System.out.println("hs.remove(valeur2)=" + hs.remove(valeur2));
    System.out.println("hs.size()=" + hs.size());
  }
}
Résultat :
hs.size()=3
hs.contains(valeur2)=false
hs.remove(valeur2)=false
hs.size()=3

La nouvelle instance n'est pas retrouvée dans la collection car la méthode equals() n'est pas redéfinie : c'est donc celle héritée de la classe Object qui est utilisée. Comme celle-ci compare les références et qu'elles sont différentes, l'objet n'est pas trouvé.

Le même exemple est utilisé mais maintenant les méthodes equals() et hashCode() des objets insérés dans la collection sont redéfinies.

Exemple :
public class Valeur {
  private final int valeur;
 
  public Valeur(int valeur) {
    super();
    this.valeur = valeur;
  }
 
  public int getValeur() {
    return valeur;
  }
 
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + valeur;
    return result;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Valeur other = (Valeur) obj;
    if (valeur != other.valeur)
      return false;
    return true;
  }
}
Résultat :
hs.size()=3
hs.contains(valeur2)=true
hs.remove(valeur2)=true
hs.size()=2

Pour retrouver une instance dans une collection, le plus simple est de parcourir tous les éléments jusqu'à ce que l'on trouve l'élément ou que tous les éléments aient été parcourus. Malheureusement, cette solution n'est pas la plus performante puisque son temps d'exécution maximum est proportionnel au nombre d'éléments dans la collection.

Pour optimiser cette recherche, les collections utilisent la valeur de la méthode hashCode() pour regrouper les éléments ayant la même valeur ou appartenant à une plage de valeurs. Le test d'égalité est ainsi fait sur l'ensemble des éléments qui font partis du même groupe.

Il est donc nécessaire que la répartition des éléments selon leurs valeurs de hachage soit diversifiée et que les groupes soient de tailles similaires : si tous les éléments ont la même valur de hachage, cela revient à parcourir tous les éléments. Si la répartition en groupe est mal organisée, par exemple par manque de dispersion dans les valeurs de hachage, l'algorithme de recherche sera meilleur mais pas encore assez performant notamment si la taille de la collection est importante.

Lors de la recherche d'un élément, sa valeur de hachage est utilisée pour déterminer le groupe d'appartenance, l'objet est recherché dans ce groupe plutôt que dans toute la collection. L'algorithme de recherche est ainsi optimisé.

Il est donc important que les éléments qui servent de clés dans une collection de type HashXXX possèdent une valeur de hachage qui soit suffisamment discriminante pour permettre une bonne répartition dans les groupes.

Il est nécessaire de garder à l'esprit que si l'implémentation de la méthode hashCode() renvoie un valeur différente selon l'état de l'objet, cela peut poser des problèmes lors de l'utilisation comme clé dans une collection de type Hashxxx. Dans ce cas, il ne faut pas que la valeur du hashcode d'un objet utilisé comme clé change car les collections de type Hashxxx présument que la valeur de hachage d'un objet utilisé comme clé ne change pas. Il est donc préférable d'utiliser, comme clé, un objet de type String ou un Wrapper qui sont des objets immuables dont la valeur de hachage ne change pas.

Par exemple, une collection de type HashTable utilise le hashcode des objets servant de clés pour déterminer dans quel groupe l'objet sera rangé. Une HashTable est créée avec un nombre arbitraire de groupes. Pour déterminer dans quel groupe insérer un objet, elle utilise le reste de la division de la valeur de hash de l'objet par le nombre de groupes. Si tous les objets ont la même valeur de hachage, tous les objets sont insérés dans le même groupe ce qui inhibe les avantages de la répartition des objets en groupes.

Lors de la recherche d'un élément à partir d'une clé, son hash code est utilisé pour déterminer dans quel groupe la recherche doit être faite. Les performances sont améliorées puisque la recherche se fait uniquement dans les éléments du groupe plutôt que sur toutes les clés de la collection.

Il est donc très important que la valeur de hachage d'un élément inséré dans une collection ne change pas sinon il y a un risque que l'objet ne soit plus retrouvé car il pourrait ne plus être dans le groupe correspondant au hashcode utilisé à son insertion. Il est cependant possible de demander un recalcul de la répartition des objets dans les groupes en utilisant la méthode rehash().

 

97.2.5.2. Les performances en définissant correctement la méthode hashCode()

Il est très important de redéfinir correctement la méthode hashCode() pour améliorer les performances lors de l'utilisation de ces objets notamment avec des collections de type HashXXX. Pour le vérifier, deux classes vont être utilisées avec deux implémentations différentes de la méthode hashCode(). Des instances de ces classes vont être insérées dans des collections de type HashTable et HashMap.

Exemple :
import java.util.Hashtable;
import java.util.concurrent.TimeUnit;

public class TestHashTablePeformance {
  
  public static void main(String[] args) {
    testAvecHashTable();
  }
  
  public static void testAvecHashTable() {
    Hashtable<Personne, String> hashTable = new Hashtable<Personne, String>();
    Personne personne = null;
    long debut = System.nanoTime();
    for (int i = 0; i < 20000; i++) {
      personne = new Personne("nom" + i, "prenom" + i, i, null, true);
      hashTable.put(personne, "nom" + i + " prenom" + i);
    }
    long fin = System.nanoTime();
    System.out.println("HashTable temps d'insertion  = "
        + TimeUnit.NANOSECONDS.toMillis(Math.abs(fin - debut)) + " ms");
    debut = System.nanoTime();
    personne = new Personne("nom12345", "prenom12345", 12345, null, true);

    if (hashTable.containsKey(personne)) {
      System.out.println(hashTable.get(personne));
    }
    fin = System.nanoTime();
    System.out.println("HashTable temps de recherche = "
        + TimeUnit.NANOSECONDS.toMillis(Math.abs(fin - debut)) + " ms");
  }
}
Exemple :
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

public class TestHashMapPeformance {

  public static void main(String[] args) {
    testAvecHashMap();
  }

  public static void testAvecHashMap() {
    HashMap<Personne, String> hashMap = new HashMap<Personne, String>();
    Personne personne = null;
    long debut = System.nanoTime();
    for (int i = 0; i < 20000; i++) {
      personne = new Personne("nom" + i, "prenom" + i, i, null, true);
      hashMap.put(personne, "nom" + i + " prenom" + i);
    }
    long fin = System.nanoTime();
    System.out.println("HashMap temps d'insertion  = "
        + TimeUnit.NANOSECONDS.toMillis(Math.abs(fin - debut)) + " ms");
    debut = System.nanoTime();
    personne = new Personne("nom12345", "prenom12345", 12345, null, true);
    if (hashMap.containsKey(personne)) {
      System.out.println(hashMap.get(personne));
    }
    fin = System.nanoTime();
    System.out.println("HashMap temps de recherche = "
        + TimeUnit.NANOSECONDS.toMillis(Math.abs(fin - debut)) + " ms");
  }
}

Les deux classes effectuent les mêmes traitements en remplissant une collection avec 20000 occurrences d'une classe dont l'id est la clé puis recherchent une occurrence particulière dans la collection.

Dans les deux exécutions suivantes, la classe Personne utilisée implémente la méthode hashCode() en renvoyant la même valeur quelque soit l'instance.

Exemple :
import java.util.Date;

public class Personne {
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;

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

  public Personne(String nom, String prenom, long id, Date dateNaiss,
      boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Personne other = (Personne) obj;
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
        return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
        return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }
}

Les performances des exécutions sont particulièrement mauvaises.

Résultat :
HashTable
temps d'insertion  = 15517 ms
HashTable
temps de recherche = 4 ms
Résultat :
HashMap temps d'insertion  = 14271 ms
HashMap temps de recherche = 4 ms

Si toutes les instances des clés renvoient les mêmes valeurs de hachage cela fonctionne, mais tous les objets sont dans le même groupe et la recherche de l'objet concerné doit se faire en invoquant la méthode equals() sur chaque objet jusqu'à trouver le bon. Cette recherche est aussi nécessaire lors de l'insertion pour vérifier si la clé n'est pas déjà présente dans la collection. Ainsi à chaque nouvelle insertion, toutes les occurrences des clés doivent être testées ce qui dégrade fortement les performances.

Dans les deux exemples suivants, l'implémentation de la méthode hashCode() de la classe Personne permet une meilleure répartition des valeurs calculées selon les valeurs des propriétés de l'instance.

Exemple :
import java.util.Date;

public class Personne {
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (adulte ? 1231 : 1237);
    result = prime * result + ((dateNaiss == null) ? 0 :
    dateNaiss.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((nom == null) ? 0 : nom.hashCode());
    result = prime * result + ((prenom == null) ? 0 : prenom.hashCode());
    return result;
  }

  public Personne(String nom, String prenom, long id, Date dateNaiss,
      boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Personne other = (Personne) obj;
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
        return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
        return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }
}

Les performances sont grandement améliorées avec une implémentation correcte de la méthode hashCode().

Résultat :
HashTable temps d'insertion  = 88 ms
HashTable temps de recherche = 0 ms
Résultat :
HashMap temps d'insertion  = 83 ms
HashMap temps de recherche = 0 ms

 

97.2.6. Des implémentations particulières des méthodes hashCode() et equals()

Cette section détaille quelques implémentations particulières des méthodes equals() et hashCode().

 

97.2.6.1. Les méthodes hashCode() et equals() dans le JDK

Les classes de l'API Java, notamment celles qui sont immuables, redéfinissent leurs méthodes hashCode() avec des algorithmes dédiés.

Comme les objets de type String et Integer sont immuables et que leurs méthodes equals() et hashCode() sont redéfinies, des instances de ces objets peuvent parfaitement servir de clés dans une collection de type HashTable, HashMap ou HashSet. Ceci est vrai aussi pour toutes les classes de type wrapper de valeurs primitives.

Les classes du JDK redéfinissent ou non leur méthode equals() selon leur besoin :

Attention à bien consulter la Javadoc pour être sûr de l'implémentation de la méthode equals() et s'éviter ainsi des problèmes. Par exemple, l'implémentation de la méthode equals() pour la classe BigDecimal teste l'égalité de la valeur encapsulée mais aussi le nombre de décimales. Le test ne se fait pas uniquement sur la valeur.

 

97.2.6.2. Les méthodes equals() et hashCode() dans une classe fille

Il faut généralement redéfinir la méthode equals() et donc la méthode hashCode() dans une classe fille si celle-ci contient des attributs supplémentaires.

Par exemple, si la classe Joueur hérite de la classe Personne et définit deux attributs supplémentaires nommés classement et nationalité, il faut redéfinir les méthodes equals() et hashCode() pour tenir compte de ces deux champs.

Exemple :
import java.util.Date;

public class Personne {
 
  private final String nom;
  private final String prenom;
  private final long id;
  private final Date dateNaiss;
  private final boolean adulte;
 
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (adulte ? 1231 : 1237);
    result = prime * result + ((dateNaiss == null) ? 0 :
    dateNaiss.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((nom == null) ? 0 : nom.hashCode());
    result = prime * result + ((prenom == null) ? 0 : prenom.hashCode());
    return result;
  }
 
  public Personne(String nom, String prenom, long id, Date dateNaiss, 
    boolean adulte) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.id = id;
    this.dateNaiss = dateNaiss;
    this.adulte = adulte;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Personne other = (Personne) obj;
    if (adulte != other.adulte)
      return false;
    if (dateNaiss == null) {
      if (other.dateNaiss != null)
        return false;
    } else if (!dateNaiss.equals(other.dateNaiss))
      return false;
    if (id != other.id)
      return false;
    if (nom == null) {
      if (other.nom != null)
         return false;
    } else if (!nom.equals(other.nom))
      return false;
    if (prenom == null) {
      if (other.prenom != null)
         return false;
    } else if (!prenom.equals(other.prenom))
      return false;
    return true;
  }
}
Exemple :
import java.util.Date;

public class Joueur extends Personne {
  private final String nationnalite;
  private final int classement;
 
  public Joueur(String nom, String prenom, long id, Date dateNaiss,
    boolean adulte, String nationnalite, int classement) {
    super(nom, prenom, id, dateNaiss, adulte);
    this.nationnalite = nationnalite;
    this.classement = classement;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + classement;
    result = prime * result + ((nationnalite == null) ? 0 : nationnalite.hashCode());
    return result;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (!super.equals(obj))
      return false;
    if (getClass() != obj.getClass())
      return false;
    Joueur other = (Joueur) obj;
    if (classement != other.classement)
      return false;
    if (nationnalite == null) {
      if (other.nationnalite != null)
        return false;
    } else if (!nationnalite.equals(other.nationnalite))
        return false;
      return true;
  }
}

Il est aussi important de bien veiller à respecter les spécifications de la méthode equals() notamment lors de la définition d'une classe fille.

Il est aussi préférable d'invoquer la méthode equals() de la classe mère et de tenir compte de son résultat dans l'implémentation de la méthode equals() de la classe fille.

Il y a deux façons de tester le type des objets comparés dans une implémentation de la méthode equals() :

Généralement le test en utilisant la méthode getClass() est plus robuste.

Le test sur l'égalité des classes en utilisant l'opérateur instanceof peut poser problème avec l'héritage. Les exemples ci-dessous vont définir deux classes, dont une hérite de l'autre, qui utilisent l'opérateur instanceof dans leur redéfinition de la méthode equals().

Exemple :
public class ClasseMere {
 
  protected String champsA;
 
  public ClasseMere(String champsA) { 
    super();
    this.champsA = champsA;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
       return false;
    if (!(obj instanceof ClasseMere))
      return false;
    ClasseMere other = (ClasseMere) obj;
    if(champsA == null) {
      if (other.champsA != null)
        return false;
    }
    else if (!champsA.equals(other.champsA))
         return false;
    return true;
  }
 
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((champsA == null) ? 0 : champsA.hashCode());
    return result;
  }
}
Exemple :
public class ClasseFille extends ClasseMere {
  protected String champsB;
 
  public ClasseFille(String champsA, String champsB) {
    super(champsA);
    this.champsB = champsB;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (!(obj instanceof ClasseFille))
      return false;
    if (!super.equals(obj))
      return false;
    ClasseFille other = (ClasseFille) obj;
    if (champsB == null) {
      if (other.champsB != null)
        return false;
    }
    else if (!champsB.equals(other.champsB))
        return false;
      return true;
  }
 
  @Override
  public int hashCode() {
   
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((champsB == null) ? 0 : champsB.hashCode());
    return result;
  }
}
Exemple :
public class TestEqualsClasseMereFille {
 
  public static void main(String[] args) {
   
    ClasseMere classeMere = new ClasseMere("champsA");
    ClasseFille classeFille = new ClasseFille("champsA","champsB");
   
    System.out.println("classeMere.equals(new ClasseMere(\"champsA\"))="
      + classeMere.equals(new ClasseMere("champsA")));
   
    System.out.println("classeFille.equals(new ClasseFille"
      + "(\"champsA\", \"champsB\"))="
      + classeFille.equals(new ClasseFille("champsA", "champsB")));
   
    System.out.println("classeMere.equals(classeFille)="
      + classeMere.equals(classeFille));
   
    System.out.println("classeFille.equals(classeMere)="
      + classeFille.equals(classeMere));
  }
}
Exemple :
classeMere.equals(new
ClasseMere("champsA"))=true
classeFille.equals(new ClasseFille("champsA", "champsB"))=true
classeMere.equals(classeFille)=true
classeFille.equals(classeMere)=false

Lorsque les objets sont de même type, toutes les règles d'implémentation de la méthode equals() sont respectées.

Cependant, lorsque la classe d'un des deux objets hérite de l'autre, la règle concernant la symétrie n'est pas respectée car un objet de type ClasseFille est bien une instance de type ClasseMere mais un objet de type ClasseMere n'est pas une instance de type ClasseFille.

Dans ce cas de figure, il est préférable de comparer l'égalité des classes plutôt que d'utiliser l'opérateur instanceof.

Généralement la règle de symétrie que doit respecter la méthode equals() est violée si une classe fille utilise la méthode getClass() dans l'implémentation de sa méthode et que la méthode equals() de sa classe mère utilise l'opérateur instanceof. La règle de symétrie est aussi violée dans le cas inverse, si une classe fille utilise l'opérateur instanceof dans l'implémentation de sa méthode et que la méthode equals() de sa classe mère utilise la méthode getClass(). Il faut conserver la même stratégie utilisée dans les classes mères et filles.

Si l'implémentation de la méthode equals() utilise l'opérateur instanceof alors il est préférable que la classe soit final ou que la méthode equals() soit final. Dans ce dernier cas, la classe pourra être dérivée mais sa méthode equals() ne pourra pas être redéfinie : la classe fille pourra alors ajouter de nouveaux comportements mais ne pourra pas ajouter de propriétés qui soient discriminantes lors du test de l'égalité d'instances.

L'implémentation de la méthode equals() qui teste l'égalité des types en utilisant la méthode getClass() est plus robuste car elle permet un respect des règles que doit mettre en oeuvre la méthode equals().

Sémantiquement les deux approches sont différentes : l'utilisation d'instanceof permet des tests entre une classe et ses classes filles. Le test avec des classes filles est parfois souhaitable notamment si la classe fille ne possède aucun nouveau champs et ajoute simplement des méthodes.

Le choix de l'approche à utiliser dépend donc de la sémantique de la classe et de la façon dont elle peut être dérivée.

 

97.2.6.3. La redéfinition des méthodes equals() et hashCode() pour des entités

La redéfinition des méthodes hashCode() et equals() de classes de type entité utilisées avec des solutions ORM comme Hibernate est assez délicate car plusieurs points sont à prendre en compte.

Les valeurs d'une entité peuvent être modifiées lors de la persistance de son état dans la base de données notamment lorsque l'entité n'existe pas encore dans la base de données : l'identifiant de l'entité est généralement créé par la base de données en utilisant un champ auto-incrémenté.

Il est donc généralement préférable de ne pas utiliser la propriété qui est l'identifiant de l'entité dans la redéfinition de la méthode hashCode(). Son utilisation est possible dans la méthode equals(). Il est fortement recommandé de ne pas modifier la valeur du hash code d'une instance : cependant, par définition les entités sont modifiables, même le champ servant d'identifiant lorsque l'entité est sauvegardée dans la base de données pour la première fois.

Ceci rend l'implémentation de la méthode hashCode() particulièrement délicate car il faudrait utiliser des propriétés qui seraient susceptibles de ne pas être modifiées. Ce n'est pas grave si plusieurs instances possèdent le même hash code mais il est très important que l'implémentation de la méthode equals() soit la plus réaliste et la plus précise possible.

Le lazy loading utilise des proxys qui sont des sous-classes de l'entité concernée. Dans ce cas, le test de l'égalité des types des classes en utilisant la méthode getClass() sur les deux instances dans la redéfinition de la méthode equals() va toujours renvoyer false.

De plus, les proxys ont des propriétés dont la valeur peut changer : ces propriétés ont leur valeur par défaut tant que les données ne sont pas chargées de la base de données. Généralement, ce chargement ce fait lors de l'invocation du premier getter sur le proxy de l'entité.

Pour limiter les risques de problèmes, il est généralement préférable d'utiliser dans la redéfinition des méthodes hashCode() et equals() les getters de propriétés plutôt que les propriétés elles-mêmes.

Pour des entités, il est généralement préférable de ne pas utiliser, dans l'implémentation des méthodes hashCode() et equals(), les collections qui encapsulent des relations mère/fille.

 

97.3. Le clonage d'un objet

Le clonage d'un objet permet de créer une nouvelle instance qui soit une copie de l'état de l'objet original : il permet donc de réaliser une copie d'un objet en le dupliquant. Cette duplication crée une nouvelle instance et copie les propriétés par valeur.

La copie d'un objet peut se faire de deux manières principales :

Il n'y a pas de règles absolues dans l'utilisation de la copie de surface ou la copie profonde : la mise en oeuvre de l'une ou de l'autre dépend essentiellement des besoins et du contexte d'utilisation.

Si l'instance à copier ne contient que des propriétés de types primitifs alors il faut utiliser une copie de surface. Si l'instance contient des références vers d'autres objets alors l'utilisation d'une copie de surface ou profonde dépend des besoins.

La copie profonde n'est pas toujours triviale à implémenter notamment si le graphe d'objets est complexe. Par exemple, s'il contient une référence circulaire ou si plusieurs références pointent sur la même instance. Il est parfois plus facile d'utiliser la sérialisation pour créer une copie profonde d'un graphe d'objets surtout si ce graphe est complexe.

Certains objets n'ont pas besoin d'être copiés : c'est le cas des objets immuables. Ainsi, il n'est pas nécessaire de cloner des objets de type String.

Il faut faire attention lors de la copie de certains objets : par exemple, il est primordial d'empêcher le clonage d'une instance dont la classe est une implémentation du motif de conception singleton. Comme, il ne devrait y avoir qu'une seule instance de cette classe, son implémentation ne devrait pas permettre sa copie. Si tel n'est pas le cas, il ne faut pas copier cette instance. Si une des classes mère redéfinie la méthode clone(), il est nécessaire de la redéfinir dans la classe du singleton pour par exemple qu'elle lève une exception de type CloneNotSupportedException.

Le but de clonage est de réaliser une copie d'un objet : en Java, il n'y a pas forcement de garantie que cette copie soit exacte. L'utilisation de la copie d'un objet doit donc être utilisée avec discernement et en tout état de cause.

Par défaut, le clonage d'un objet en Java se fait grâce à une copie de surface qui se fait champ par champ. Pour une copie profonde, il est nécessaire d'écrire du code pour réaliser l'opération.

La copie d'un objet ne doit pas être utilisée pour créer et initialiser de nouvelles instances. Par défaut, le constructeur n'est pas invoqué lors de la copie.

 

97.3.1. L'interface Cloneable

L'interface Cloneable est un marqueur que toute classe doit implémenter pour permettre à ses instances de pouvoir être clonées par le mécanisme standard. Elle ne définit aucune méthode : elle permet simplement de préciser que les instances peuvent être clonées.

La classe Object n'implémente pas l'interface Cloneable : l'invocation de la méthode clone() sur une instance de type Object lèvera donc toujours une exception de type CloneNotSupportedException.

 

97.3.2. Le clonage par copie de surface

Par défaut en Java, il n'est pas possible de créer une copie d'un objet. L'API Java propose un support de la fonctionnalité de clonage par copie de surface d'un objet en utilisant la méthode clone().

Par défaut, aucun objet n'est clonable. Pour pouvoir être clonée, la classe d'un objet doit respecter deux conditions :

La méthode clone() est définie dans la classe Object mais elle est déclarée avec le modificateur protected.

Il est donc nécessaire de la redéfinir dans une classe en la déclarant avec le modificateur public pour permettre son accès. L'implémentation de la méthode clone() est à la discrétion du développeur. Pour une copie de surface, il est possible d'invoquer la méthode clone() de la classe Object en invoquant la méthode parent dans chaque classe. Il est aussi possible d'utiliser des traitements particuliers à la copie.

La méthode clone() de la classe Object vérifie que la classe implémente l'interface Cloneable : si ce n'est pas le cas alors elle lève une exception de type CloneNotSupportedException.

La méthode clone() de la classe Object renvoie un objet de type Object. A partir de Java 5, il est possible d'utiliser le support de la covariance pour que la méthode clone() renvoie le type de l'instance copiée.

 

97.3.2.1. La redéfinition de la méthode clone()

La méthode clone() de la classe Object est définie avec les modificateurs native et protected.

L'implémentation de la méthode clone() dans la classe Object effectue plusieurs opérations :

Ce comportement par défaut peut être modifié en redéfinition la méthode clone() dans la classe concernée.

Pour pouvoir réaliser un clonage, il faut redéfinir la méthode clone() héritée de la classe Object. Cette méthode est déclarée protected dans la classe Object et doit être redéfinie avec la visibilité public.

Exemple :
public class MaClasse implements Cloneable {
      
  private int    monEntier;
  private String maChaine;

  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }

  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    Object resultat = null;
    try {
      resultat = super.clone();
    } catch (CloneNotSupportedException cnse) {
      cnse.printStackTrace();
    }
    return resultat;
  }
}

L'implémentation de la méthode clone() doit respecter plusieurs règles qui sont définies par convention :

Si la classe ne possède pas de propriétés qui soient des objets, alors le plus simple est d'invoquer l'implémentation de la classe Object.

Pour effectuer le clonage, il suffit d'invoquer la méthode clone() de l'objet concerné.

Exemple :
public class TestClone {
			
  public static void main(String[] args) {
    MaClasse original = new MaClasse();
    original.setMaChaine("maChaine");
    original.setMonEntier(10);
    try {
      MaClasse clone = (MaClasse) original.clone();
      System.out.println("machaine=" + clone.getMaChaine());
      System.out.println("monEntier=" + clone.getMonEntier());
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
machaine=maChaine
monEntier=10

A partir de Java 5, il est inutile de caster le résultat de l'invocation de la méthode clone() car avec le support de la covariance, la redéfinition de ma méthode clone() peut renvoyer le type de la classe.

Exemple (Java 5) :
public class MaClasse implements Cloneable {
			
  @Override
  public MaClasse clone() throws CloneNotSupportedException {
    return (MaClasse) super.clone();
  }
}

Attention : lors de l'invocation de la méthode clone() de la classe Object, le constructeur de la classe n'est pas invoqué lors de la création de la nouvelle instance.

Exemple :
public class MaClasse implements Cloneable {

  private int    monEntier;
  private String maChaine;

  public MaClasse() {
    System.out.println("MaClasse constructeur");
  }
  // ...

}
Exemple :
public class TestClone {
  public static void main(String[] args) {
    MaClasse original = new MaClasse();
    original.setMaChaine("maChaine");
    original.setMonEntier(10);
    try {
      MaClasse clone = (MaClasse) original.clone();
      System.out.println("machaine=" + clone.getMaChaine());
      System.out.println("monEntier=" + clone.getMonEntier());
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
MaClasse constructeur
machaine=maChaine
monEntier=10

Comme l'interface Cloneable ne définit aucune méthode, une classe qui l'implémente n'a pas d'obligation à redéfinir la méthode clone() : c'est juste une convention qui ne sera pas vérifiée par le compilateur.

Si la méthode redéfinit la méthode clone() mais n'implémente pas l'interface Cloneable alors une exception de type CloneNotSupportedException est levée.

Exemple :
public class MaClasse {
			
  @Override
  public MaClasse clone() throws CloneNotSupportedException {
    return (MaClasse) super.clone();
  }
}
Exemple :
public class TestClone {
      
  public static void main(String[] args) {
    MaClasse original = new MaClasse();
    try {
      MaClasse clone = original.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
java.lang.CloneNotSupportedException: MaClasse
                at java.lang.Object.clone(Native Method)
                at MaClasse.clone(MaClasse.java:4)
                at TestClone.main(TestClone.java:6)

L'utilisation de la méthode clone() possède un gros inconvénient : elle n'est généralement pas définit dans une interface ou une classe abstraite fille. Ceci empêche l'invocation de la méthode clone() en cas d'utilisation du polymorphisme. Dans ce cas, il faut que le type de l'objet à copier contienne la méthode clone(). Ceci réduit la possibilité de mettre en oeuvre le principe d'abstraction qui suggère d'utiliser le type le plus générique possible.

Par exemple, il n'est pas possible d'invoquer la méthode clone() sur un objet de type List car l'interface List ne définit pas la méthode clone(). Cependant les classes ArrayList ou LinkedList redéfinissent la méthode clone() avec le modificateur public.

Dans ce cas, il est impératif d'utiliser le type concret de la classe au lieu d'une de ses interfaces ou classes abstraites : ceci va à l'encontre de la mise en oeuvre du principe de l'abstraction.

 

97.3.2.2. L'implémentation personnalisée de la méthode clone()

Il est possible de gérer la copie en écrivant son propre code.

Exemple :
package com.jmdoudoux.test.clone;
			
public class MaClasse implements Cloneable {
  private int    monEntier;
  private String maChaine;
  
  public MaClasse() {
    System.out.println("MaClasse constructeur");
  }
  
  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }
  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }

  @Override
  public Object clone() throws CloneNotSupportedException {
    MaClasse result = new MaClasse();
    result.setMaChaine(this.maChaine);
    result.setMonEntier(this.monEntier);
    return result;
  }
}

 

97.3.3. Le clonage par copie profonde

Pour réaliser une copie profonde (deep copy), son implémentation doit réaliser une copie des références de manière récursive.

Pour effectuer une copie profonde, il est nécessaire de parcourir tous les objets qui composent le graphe et créer une copie de chacun d'entre-eux.

Exemple :
package com.jmdoudoux.test.clone;
			
public class MaClasse implements Cloneable {
 
  private int    monEntier;
  private String maChaine;
  private Groupe groupe;
 
  public MaClasse() { 
    System.out.println("MaClasse constructeur");
  }
  
  public int getMonEntier() {
    return monEntier;
  }
  
  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }
 
  public void setMaChaine(String maChaine) { 
    this.maChaine = maChaine;
  }
 
  public Groupe getGroupe() {
    return groupe;
  }
 
  public void setGroupe(Groupe groupe) {
    this.groupe = groupe;
  }

  @Override
  public MaClasse clone() throws CloneNotSupportedException {
    MaClasse result = new MaClasse();
    result.setMaChaine(this.maChaine);
    result.setMonEntier(this.monEntier);
    Groupe original = this.getGroupe();
    Groupe groupe = new Groupe(original.getId(), original.getNom());
    result.setGroupe(groupe);
    return result;
  }
}
Exemple :
package com.jmdoudoux.test.clone;

public class TestDeepCloneAvecSerialisation {
  
  public static void main(String[] args) {
    Groupe groupe = new Groupe(1, "groupe 1");
    groupe.setId(1);
    groupe.setNom("groupe 1");
    MaClasse maClasse1 = new MaClasse();
    maClasse1.setMaChaine("maChaine1");
    maClasse1.setMonEntier(10);
    maClasse1.setGroupe(groupe);
    System.out.println(groupe);
    try {
      MaClasse copie1 = maClasse1.clone();
      System.out.println(copie1.getMaChaine());
      System.out.println(copie1.getGroupe());
      System.out.println(copie1.getGroupe().getNom());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
Résultat :
MaClasse constructeur
com.jmdoudoux.test.clone.Groupe@42719c
MaClasse constructeur
maChaine1
com.jmdoudoux.test.clone.Groupe@30c221
groupe 1

Lors de l'implémentation d'un mécanisme de copie profonde, il est nécessaire de faire attention aux références circulaires.

Il faut aussi faire attention à certains types d'objets qui ne doivent pas être clonés car ils encapsulent des ressources.

Lors de l'utilisation d'une copie profonde, il est donc nécessaire d'avoir une bonne connaissance des objets qui composent le graphe.

 

97.3.4. Le clonage en utilisant la sérialisation

Lorsque le graphe d'objet à copier est important ou complexe, il n'est pas toujours facile d'écrire le code nécessaire à la réalisation de l'opération. Java propose en standard la sérialisation qui permet de stocker l'état d'un graphe d'objets et d'en recréer des instances avec le même état.

Il est donc possible d'utiliser la sérialisation comme mécanisme pour l'implémentation d'une copie profonde. Cette solution est une dès plus simple à mettre en oeuvre.

Cependant, il faut que les types des objets liés dans le graphe puissent être sérialisés. Si tel est le cas, alors la sérialisation permet de facilement réaliser une copie profonde d'un objet.

Exemple :
package com.jmdoudoux.test.clone;
			
import java.io.Serializable;

public class Groupe implements Serializable {

  private static final long serialVersionUID = 1L;
  private long              id;
  private String            nom;

  public Groupe(long id, String nom) {
    super();
    this.id = id;
    this.nom = nom;
  }
  
  public long getId() {
    return id;
  }
  
  public void setId(long id) {
    this.id = id;
  }
  
  public String getNom() {
    return nom;
  }
  
  public void setNom(String nom) {
    this.nom = nom;
  }
}
Exemple :
package com.jmdoudoux.test.clone;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MaClasse implements Serializable {
  private static final long serialVersionUID = 1L;
  private int               monEntier;
  private String            maChaine;
  private Groupe            groupe;

  public MaClasse() {
    System.out.println("MaClasse constructeur");
  }

  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }

  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }

  public Groupe getGroupe() {
    return groupe;
  }

  public void setGroupe(Groupe groupe) {
    this.groupe = groupe;
  }

  public MaClasse dupliquer() throws Exception {
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      oos = new ObjectOutputStream(baos);
      oos.writeObject(this);
      ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
      ois = new ObjectInputStream(bais);
      return (MaClasse) ois.readObject();
    } finally {
      if (oos != null)
        oos.close();
      if (ois != null)
        ois.close();
    }
  }
}

Si tous les objets du graphe à dupliquer sont Serializable alors il est possible d'utiliser l'API de sérialisation pour créer une copie des objets du graphe. Pour limiter la dégradation des performances lors de la copie, il est possible de réaliser l'opération dans un flux en mémoire grâce à un objet de type ByteArrayOutputStream et un objet de type ByteArrayInputSteam.

Exemple :
package com.jmdoudoux.test.clone;
			
public class TestDeepCloneAvecSerialisation {

  public static void main(String[] args) {
    Groupe groupe = new Groupe(1, "groupe 1");
    groupe.setId(1);
    groupe.setNom("groupe 1");
    MaClasse maClasse1 = new MaClasse();
    maClasse1.setMaChaine("maChaine1");
    maClasse1.setMonEntier(10);
    maClasse1.setGroupe(groupe);
    System.out.println(groupe);
    try {
      MaClasse copie1 = maClasse1.dupliquer();
      System.out.println(copie1.getMaChaine());
      System.out.println(copie1.getGroupe());
      System.out.println(copie1.getGroupe().getNom());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

L'utilisation de la sérialisation pour la copie d'un objet possède plusieurs inconvénients :

 

97.3.5. Le clonage de tableaux

Les tableaux sont des objets qui peuvent être clonés. L'implémentation de leur méthode clone() ne lève pas d'exception de type CloneNotSupportedException puisque tous les tableaux peuvent être clonés.

A partir de Java 5, il est aussi inutile de caster le résultat de l'invocation de la méthode clone() sur un tableau.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.clone;

public class TestCloneTableau {
  
  public static void main(String[] args) {
    MaClasse maClasse1 = new MaClasse();
    maClasse1.setMaChaine("maChaine1");
    maClasse1.setMonEntier(10);
    MaClasse maClasse2 = new MaClasse();
    maClasse2.setMaChaine("maChaine2");
    maClasse2.setMonEntier(20);
    MaClasse[] tableauOriginal = { maClasse1, maClasse2 };
    MaClasse[] tableauClone = tableauOriginal.clone();
    System.out.println("tableauOriginal=" + tableauOriginal);
    System.out.println("tableauClone=" + tableauClone);
    System.out.println("tableauOriginal[0]=" +tableauOriginal[0]);
    System.out.println("tableauClone[0]=" + tableauClone[0]);
  }
}
Résultat :
tableauOriginal=[Lcom.jmdoudoux.test.clone.MaClasse;@187aeca
tableauClone=[Lcom.jmdoudoux.test.clone.MaClasse;@e48e1b
tableauOriginal[0]=com.jmdoudoux.test.clone.MaClasse@12dacd1
tableauClone[0]=com.jmdoudoux.test.clone.MaClasse@12dacd1

Lors du clonage d'un tableau, c'est une copie de surface qui est effectuée. Un nouveau tableau est créé mais les objets contenus dans les tableaux sont les mêmes.

Il faut écrire du code pour effectuer une copie profonde du tableau.

Exemple :
package com.jmdoudoux.test.clone;
			
public class TestCloneTableau {

  public static void main(String[] args) {
    MaClasse maClasse1 = new MaClasse();
    maClasse1.setMaChaine("maChaine1");
    maClasse1.setMonEntier(10);
    MaClasse maClasse2 = new MaClasse();
    maClasse2.setMaChaine("maChaine2");
    maClasse2.setMonEntier(20);
    MaClasse[] tableauOriginal = { maClasse1, maClasse2 };
    MaClasse[] tableauClone = new MaClasse[tableauOriginal.length];
    for (int i = 0; i < tableauOriginal.length; i++) {
      MaClasse maClasse = new MaClasse();
      maClasse1.setMaChaine(tableauOriginal[i].getMaChaine());
      maClasse1.setMonEntier(tableauOriginal[i].getMonEntier());
      tableauClone[i] = maClasse;
    }
    System.out.println("tableauOriginal=" + tableauOriginal);
    System.out.println("tableauClone=" + tableauClone);
    System.out.println("tableauOriginal[0]=" + tableauOriginal[0]);
    System.out.println("tableauClone[0]=" + tableauClone[0]);
  }
}
Résultat :
tableauOriginal=[Lcom.jmdoudoux.test.clone.MaClasse;@18a992f
tableauClone=[Lcom.jmdoudoux.test.clone.MaClasse;@4f1d0d
tableauOriginal[0]=com.jmdoudoux.test.clone.MaClasse@1fc4bec
tableauClone[0]=com.jmdoudoux.test.clone.MaClasse@dc8569

 

97.3.6. La duplication d'un objet en utilisant un constructeur ou une fabrique

Il est possible d'utiliser des solutions plus classiques et moins spécifiques pour dupliquer des objets. Ces solutions nécessitent d'écrire et de maintenir plus de code mais elles permettent d'avoir un contrôle très fin sur la duplication et sont performantes à l'exécution.

 

97.3.6.1. La duplication d'un objet en utilisant un constructeur

Il est possible de créer une copie d'un objet en utilisant un constructeur qui attend en paramètre un objet du type de la classe elle-même. Le constructeur peut alors copier les valeurs des propriétés de l'objet passé paramètre dans les propriétés correspondantes.

Exemple :
public class MaClasse implements Cloneable {
			
  private int    monEntier;
  private String maChaine;

  public MaClasse(MaClasse original) {
    this.monEntier = original.monEntier;
    this.maChaine = original.maChaine;
  }

  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }

  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }
}
Exemple :
public class TestCloneConstructeur {

  public static void main(String[] args) {
    MaClasse original = new MaClasse();
    original.setMaChaine("maChaine");
    original.setMonEntier(10);
    MaClasse clone = new MaClasse(original);
    System.out.println("machaine=" + clone.getMaChaine());
    System.out.println("monEntier=" + clone.getMonEntier());
  }
}
Résultat :
machaine=maChaine 
monEntier=10

Cette solution n'est pas toujours utilisable car elle nécessite la définition d'un constructeur dédié.

 

97.3.6.2. La duplication d'un objet en utilisant une fabrique

Il est possible de créer une copie d'un objet en utilisant une fabrique qui attend en paramètre l'instance originale. La fabrique peut alors copier les valeurs des propriétés de l'objet passé paramètre dans les propriétés correspondantes d'une nouvelle instance.

Exemple :
public class MaClasse implements Cloneable {
			
  private int    monEntier;
  private String maChaine;

  public MaClasse() {
    System.out.println("MaClasse constructeur");
  }

  public static MaClasse dupliquer(MaClasse original) {
    MaClasse resultat = new MaClasse();
    resultat.monEntier = original.monEntier;
    resultat.maChaine = original.maChaine;
    return resultat;
  }

  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }

  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }
}
Exemple :
public class TestCloneFabrique {

  public static void main(String[] args) {
    MaClasse original = new MaClasse();
    original.setMaChaine("maChaine");
    original.setMonEntier(10);
    MaClasse clone = MaClasse.dupliquer(original);
    System.out.println("machaine=" + clone.getMaChaine());
    System.out.println("monEntier=" + clone.getMonEntier());
  }
}
Résultat :
MaClasse constructeur 
MaClasse constructeur
machaine=maChaine
monEntier=10

 

97.3.7. La duplication en utilisant des bibliothèques tierces

Il est possible d'utiliser des bibliothèques tierces pour faciliter ou réaliser la copie d'un objet.

 

97.3.7.1. La duplication en utilisant Apache Commons

La classe org.apache.commons.lang3.ObjectUtils propose la méthode clone() qui permet de cloner un objet. Elle facilite l'utilisation du clonage de l'objet passer en paramètre en invoquant sa méthode clone() par introspection.

L'objet passé en paramètre doit donc implémenter l'interface Cloneable.

Elle effectue aussi une copie si l'objet fourni en paramètre est un tableau d'objets.

Elle lève une exception de type CloneFailedException si son exécution échoue.

Elle renvoie null si null est passé en paramètre.

 

97.3.7.2. La duplication en utilisant Kryo

Kryo est une bibliothèque open source, diffusée sous la licence New BSD, qui permet de sérialiser un graphe d'objets. Elle permet aussi de cloner un objet sans en sérialiser les données.

Pour utiliser Kryo, il suffit d'ajouter la bibliothèque kryo.jar et ses dépendances asm, minlog et objenesis. Le plus simple est de déclarer la dépendance dans un projet Maven :

Exemple :
    <dependency>       
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo</artifactId>
        <version>3.0.0</version>
    </dependency>

La méthode copy() permet de réaliser une copie profonde (deep copy) de l'objet fourni en paramètre.

La méthode copyShallow() permet de réaliser une copie de surface (shallow copy) de l'objet fourni en paramètre.

Les références multiples à un même objet et les références circulaires sont gérées par Kryo.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.test.TestKryo;
			
import com.esotericsoftware.kryo.Kryo;

public class TestKryo {

  public static void main(String[] args) {
    Kryo kryo = new Kryo();
    Groupe groupe = new Groupe();
    groupe.setId(1);
    groupe.setNom("groupe 1");
    MaClasse maClasse1 = new MaClasse();
    maClasse1.setMaChaine("maChaine1");
    maClasse1.setMonEntier(10);
    maClasse1.setGroupe(groupe);
    System.out.println(groupe);

    MaClasse copie1 = kryo.copyShallow(maClasse1);
    System.out.println(copie1.getMaChaine());
    System.out.println(copie1.getGroupe());
    System.out.println(copie1.getGroupe().getNom());

    MaClasse copie2 = kryo.copy(maClasse1);
    System.out.println(copie2.getMaChaine());
    System.out.println(copie2.getGroupe());
    System.out.println(copie2.getGroupe().getNom());
  }
}
Résultat :
MaClasse constructeur
com.jmdoudoux.test.TestKryo.Groupe@fced4
MaClasse constructeur
maChaine1
com.jmdoudoux.test.TestKryo.Groupe@fced4
groupe 1
MaClasse constructeur
maChaine1
com.jmdoudoux.test.TestKryo.Groupe@272961
groupe 1

La classe des objets clonés n'ont pas besoin d'implémenter d'interface particulière.

 

97.3.7.3. La duplication en utilisant XStream

XStream est une API open source qui permet de sérialiser/désérialiser des objets.

Le site du projet est à l'url http://xstream.codehaus.org

Pour utiliser XStream, il suffit d'ajouter la bibliothèque xstream.jar et sa dépendance xpp3_min.jar. Le plus simple est de déclarer la dépendance dans un projet Maven :

Exemple :
    <dependency>
        <groupId>xstream</groupId>
        <artifactId>xstream</artifactId>
        <version>1.2.2</version>
    </dependency>

Pour la mise en oeuvre, il suffit de créer une instance de type XStream et d'invoquer ses méthodes toXML() et fromXML().

Exemple :
package com.jmdoudoux.test.clone;
			
public class MaClasse {
  private int    monEntier;
  private String maChaine;
  private Groupe groupe;

  public MaClasse() {
    System.out.println("MaClasse constructeur");
  }

  public int getMonEntier() {
    return monEntier;
  }

  public void setMonEntier(int monEntier) {
    this.monEntier = monEntier;
  }

  public String getMaChaine() {
    return maChaine;
  }

  public void setMaChaine(String maChaine) {
    this.maChaine = maChaine;
  }

  public Groupe getGroupe() {
    return groupe;
  }

  public void setGroupe(Groupe groupe) {
    this.groupe = groupe;
  }
}
Exemple :
package com.jmdoudoux.test.clone;

public class Groupe {
  private long   id;
  private String nom;

  public Groupe() {
  }

  public Groupe(long id, String nom) {
    super();
    this.id = id;
    this.nom = nom;
  }

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}
Exemple :
package com.jmdoudoux.test.clone;

import com.thoughtworks.xstream.XStream;

public class TestXStream {

  public static void main(String[] args) {
    XStream xstream = new XStream();
    Groupe groupe = new Groupe(1, "groupe 1");
    groupe.setId(1);
    groupe.setNom("groupe 1");
    MaClasse maClasse = new MaClasse();
    maClasse.setMaChaine("maChaine1");
    maClasse.setMonEntier(10);
    maClasse.setGroupe(groupe);
    System.out.println(groupe);
    MaClasse copie = (MaClasse) xstream.fromXML(xstream.toXML(maClasse));
    System.out.println(copie.getMaChaine());
    System.out.println(copie.getGroupe());
    System.out.println(copie.getGroupe().getNom());
  }
}
Résultat :
MaClasse constructeur
com.jmdoudoux.test.clone.Groupe@35bb0f
MaClasse constructeur
maChaine1
com.jmdoudoux.test.clone.Groupe@31688f
groupe 1

La classe des objets clonés n'ont pas besoin d'implémenter d'interface particulière par contre elles doivent impérativement posséder un constructeur sans argument sinon une exception est levée.

Résultat :
MaClasse constructeur
com.jmdoudoux.test.clone.Groupe@18bd7f1
MaClasse constructeur
Exception in thread
"main" com.thoughtworks.xstream.converters.ConversionException:
Cannot construct com.jmdoudoux.test.clone.Groupe as it does not have a no-args
constructor
---- Debugging information
----
message             : Cannot construct
com.jmdoudoux.test.clone.Groupe as it does not have a no-args constructor
cause-exception     :
com.thoughtworks.xstream.converters.reflection.ObjectAccessException
cause-message       : Cannot construct com.jmdoudoux.test.clone.Groupe
as it does not have a no-args constructor
class               : com.jmdoudoux.test.clone.MaClasse
required-type       : com.jmdoudoux.test.clone.XStream.Groupe
path                : /com.jmdoudoux.test.clone.MaClasse/groupe
line number         : 4
-------------------------------

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

72 commentaires Donner une note à l'article (5)

 

Copyright (C) 1999-2016 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.

Responsables bénévoles de la rubrique Java : Mickael Baron - Robin56 -