IP-Tech Offshore

Nos compétances, votre nouveau levier de croissance

Les objets immuables

Je voudrais, par cet article, introduire la notion d’objet immuable. J’ai choisi de commencer par une brève définition, puis d’énumérer quelques avantages de l’utilisation d’objets immuables, pour enfin, s’attarder un peu plus sur quelques exemples commentés détaillants les techniques d’implémentation.

Définition

On appelle un objet immuable, tout objet dont l’état ne change pas après sa création. On peut trouver, par abus de langage, la notion de ‘classe immuable’ et qui indique une classe dont les instances sont immuables. Dans certains cas, un objet est considéré immuable, même si certains de ses attributs (internes) changent, mais que l’état de l’objet reste inchangé d’un point de vue extérieur. Ceci peut, par exemple, être le cas de certains objets utilisant un système de cache pour mémoriser les résultats de traitements couteux (technique de memoization). Précisons, enfin, que les objets immuables sont généralement des instances de classes du modèle objet, communément appelé aussi les objets du domaine, les VO (value Object) ou les DTO (Data Transfert Object), comme Personne, Catalogue, Couleur ou Document.

Avantages

Les objets immuables présentent plusieurs avantages. Puisque leurs états ne changent pas après la construction, ils sont –intrinsèquement- non seulement thread-safe, mais peuvent aussi être mis en cache par le client (au sens objet du terme) sans risque de désynchronisation. Un autre avantage, non négligeable lui aussi, est que les objets immuables constituent des candidats parfaits pour les clés d’une Map (puisque leurs hashCode ne varie pas). Enfin, d’autres avantages peuvent être cités comme le fait qu’une simple copie de la référence constitue une implémentation parfaite du clonage de l’objet (pas besoin d’implémenter l’interface Cloneable), ce qui résulte en un gain de la mémoire utilisée (une copie de la référence occupe la taille d’un pointeur) et une vitesse d’exécution plus importante.

Implémentation

Pour implementer une classe immuable, il suffit de :

  • S’assurer qu’on ne peut pas hériter de la classe (final);
  • Rendre tous les champs privés et final afin d’en empêcher la modification;
  • Forcer les classes appelantes (clientes) à construire l’objet en une seule étape (bloquer la possibilité d’appeler un constructeur sans arguments, suivis d’appels vers d’autres méthodes comme pour la construction des JavaBean);
  • Ne publier aucune méthode susceptible de modifier l’état de l’objet, comme les modificateurs (setters);
  • Faire une copie défensive de tout objet muable, au niveau des constructeurs publics et au niveau des accesseurs (getters).

Exemple

Avant de construire quelques exemples pour illustrer une bonne implémentation d’une classe immuable, citons quelques exemples courants : Les classes java.lang.String, java.lang.Integer et presque toutes les classes d’objets valeurs de l’API Java sont des classes immuables.
C’est en effet pour cela que les méthodes permettant de modifier une instance existante ne modifient pas l’instance elle-même, mais renvoient plutot une nouvelle instance. Ceci est le cas des méthodes replace, concat et toLowerCase de la classe String. Considérons le code ci-dessous :

package com.iptech.immutable;

public class Main {

   public static void main(String[] args) {

      String maChaine = new String(“IP-TECH BLOG”);
      maChaine.replace(“BLOG”, “WEBSITE”);
      System.out.println(maChaine);

      Integer monEntier = new Integer(5);
      Integer copieDeMonEntier = monEntier;
      monEntier = monEntier * 10;
      System.out.println(copieDeMonEntier);
   }
}

En executant ce code, nous retrouvons sur la console le résultat suivant :

Resultat d'execution

Nous remarquons que le mot « BLOG » n’a pas été remplacé par « WEBSITE » dans la variable maChaine. Ceci est du au fait que la classe String est immuable, et que la méthode replace retourne une nouvelle instance de la classe String (cette nouvelle instance n’a été stockée nulle part dans notre exemple).

La deuxième ligne de la console nous montre que la variable copieDeMonEntier n’a pas été affectée par la multiplication. Ceci peut paraître normal à première vue, puisque l’on a stocké la valeur 5 dans la variable. Mais si nous nous rappelons que Integer est un Objet et non un type primitif (comme int), nous comprendrons que l’on a pas stocké la valeur 5, mais la référence vers celle-ci. Autrement dit, jusqu’à avant la ligne où on a effectué la multiplication, les variables monEntier et copieDeMonEntier contiennent la même référence (pointent vers le même objet Integer). Voyant les choses sous cet angle, nous pouvons conclure que l’opération de multiplication n’a fait que créer une toute nouvelle instance de la classe Integer, et n’a pas procédé à la modification de l’ancienne instance.

Essayons, maintenant de construire notre propre exemple : une classe Personne qui contient nom, prénom et date de naissance. En appliquant les quatre premières règles d’implémentation, nous aurons un code comme celui-ci :

package com.iptech.immutable;

import java.util.Date;

public final class Personne {

   private final String nom;
   private final String prenom;
   private final Date dateDeNaissance;

   public Personne(String nom, String prenom,
         Date dateDeNaissance) {
      this.nom = nom;
      this.prenom = prenom;
      this.dateDeNaissance = dateDeNaissance;
   }

   public String getNom() {
      return nom;
   }

   public String getPrenom() {
      return prenom;
   }

   public Date getDateDeNaissance() {
      return (Date)dateDeNaissance.clone();
   }

}

Je vais maintenant m’attarder un peu plus sur la cinquième règle qui stipule qu’il faut procéder à une copie défensive au niveau des accesseurs et au niveau des constructeurs publics pour chaque champ muable. Rappelons d’abord que la classe java.util.Date est une classe muable, et remarquons qu’une partie de cette règle a été appliquée puisque au niveau de l’accesseur de la date de naissance l’objet a été cloné avant de le renvoyer. Essayons de décortiquer les raisons de cette dernière règle (en particulier la copie au niveau du constructeur) en analysant le résultat du code suivant :

package com.iptech.immutable;

import java.util.Date;

public class Main {

   public static void main(String[] args) {

      Date date = new Date();

      Personne p = new Personne(“”,“”,date);
      System.out.println(p.getDateDeNaissance());

      //Retrancher un jour de la valeur de l’objet :
      //24 heures * 3600 secondes * 1000 millisecondes
      date.setTime(date.getTime()-(24*3600*1000));
      System.out.println(p.getDateDeNaissance());

   }

}

Le resultat est :

Resultat d'execution

Nous comprenons mieux maintenant que si l’on ne procède pas à une copie défensive au niveau du constructeur, le code client (appelant) peut garder une référence vers l’objet muable (dans cette exemple date), et donc le modifier après la construction et par conséquent modifier l’instance construite elle-même (dans cette exemple la variable p). Rappelons aussi que la cinquième règle ne s’applique pas pour les champs nom et prénom, puisqu’ils sont, comme on l’a évoqué plus haut, des objets immuables.
Enfin, je termine par un cas un peu particulier et qui nécessite un peu plus d’attention. C’est le cas où au niveau du constructeur, les données doivent être validées. Si on reprend l’exemple précédent, imaginons qu’on veuille accepter seulement les personnes dont la date de naissance ne dépasse pas celle d’aujourd’hui (on n’accepte pas les personnes qui naissent dans le futur). On aura le code suivant :

package com.iptech.immutable;

import java.util.Date;

public final class Personne {

   private final String nom;
   private final String prenom;
   private final Date dateDeNaissance;

   public Personne(String nom, String prenom,
         Date dateDeNaissance) {
      this.nom = nom;
      this.prenom = prenom;
      if(dateDeNaissance.before(new Date()))
         this.dateDeNaissance = (Date)dateDeNaissance.clone();
      else
         throw new IllegalArgumentException(“Personne” +
               ” née dans le futur !!”);
   }

   public String getNom() {
      return nom;
   }

   public String getPrenom() {
      return prenom;
   }

   public Date getDateDeNaissance() {
      return (Date)dateDeNaissance.clone();
   }

}

Bien que correcte à première vue, cette implémentation présente un problème dans un contexte multi-thread. Supposons que deux thread partagent la même référence vers un objet Date. Le premier thread essaie de construire un objet de classe Personne avec cette référence (une date valide). Lorsque le code au niveau du constructeur arrive à la quatrième ligne (date déjà validée par le test), le deuxième thread modifie l’objet date pour y mettre une valeur invalide (une date future). Le premier thread continue pour cloner cette valeur invalide et se retrouve avec une personne née dans le futur !
Pour y remédier, il suffit de cloner l’objet avant le test :

package com.iptech.immutable;

import java.util.Date;

public final class Personne {

   private final String nom;
   private final String prenom;
   private final Date dateDeNaissance;

   public Personne(String nom, String prenom,
         Date dateDeNaissance) {
      this.nom = nom;
      this.prenom = prenom;
      Date temp = (Date)dateDeNaissance.clone();
      if(temp.before(new Date()))
         this.dateDeNaissance = temp;
      else
         throw new IllegalArgumentException(“Personne” +
               ” née dans le futur !!”);
   }

   public String getNom() {
      return nom;
   }

   public String getPrenom() {
      return prenom;
   }

   public Date getDateDeNaissance() {
      return (Date)dateDeNaissance.clone();
   }

}

Références :

Tags: , , , , , ,

2 commentaires pour “Les objets immuables”

  1. aymens dit :

    Votre article est très instructif, merci.

  2. armel dit :

    J\’ai lu ce post avec beaucoup de plaisir. MERCI!

Laisser un commentaire

Security Code:


Tous les droits sont réservés pour IP-Tech