Classes et objets immuables

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Google Bookmarks ! Facebook Digg del.icio.us Yahoo MyWeb Blinklist Netvouz Reddit Simpy StumbleUpon Bookmarks Share on Google+ 

I. Introduction

Les développeurs Java connaissent tous très bien la notion de classe immuable. Beaucoup ont d'ailleurs pesté contre sa principale représentante, java.lang.String. L'expression classe immuable est en fait un abus de langage, un substitut courant pour objet immuable. Un objet dit immuable est une instance de classe dont les membres exportés (ou visibles, que cela soit par un modificateur d'accès direct ou indirect, protected, public ou package private) ne peuvent être modifiés après création.

Les classes immuables ont de nombreux avantages en leur faveur et leur utilisation simplifie parfois tellement le développement que je voulais écrire ce billet pour vous encourager à en utiliser aussi souvent que possible. De part leur propriété intrinsèque, ces objets ont un seul état. Cette dernière remarque peut vous sembler anodine mais a de nombreuses répercussions très importantes. Voici en vrac les avantages des objets immuables :

  • Ils sont garantis thread-safe
  • Ils peuvent être mis en cache
  • Ils n'ont besoin ni de constructeur par copie, ni d'implémentation de l'interface Cloneable
  • Il n'est pas nécessaire d'en faire une copie défensive
  • Leurs invariants sont testés à la création seulement
  • Ils constituent d'excellentes clés pour les Map et Set
  • Leurs valeurs peuvent être mises en cache par le client sans risque de désynchronisation

Les classes immuables sont particulièrement adaptées à la représentation de types de données abstraits. L'API de Java en contient plusieurs exemples : Integer, Color, BigDecimal, etc. La définition de type abstrait dépend toutefois de votre application. Ainsi, un logiciel affichant le contenu d'un magasin en ligne (livres, musique, DVD, etc.) pourra utiliser des classes immuables pour représenter les articles.

II. Créer une classe immuable

Écrire une classe immuable n'est pas une tâche difficile mais demande beaucoup d'attention pour ne pas exporter indirectement des valeurs. Voici les règles à suivre :

  • La classe doit être déclarée final (dans le cas contraire, il serait possible de modifier une instance par héritage)
  • Tous les champs doivent être déclarés final
  • La référence à this ne doit jamais être exportée
  • Tous les champs faisant référence à un objet non immuable doivent être privés, ne jamais être exportés, représenter l'unique référence à cet objet et ne jamais modifier l'état de l'objet

Le dernier point est le plus délicat mais évident avec un exemple :

 
Sélectionnez

private final Date theDate;

public MaClasse(Date theDate) {
  this.theDate = theDate;
}

@Override
public String toString() {
  return theDate.toString();
}

A première vue, cet exemple est correct puisque le membre theDate est privé, final et jamais exporté. En outre, la valeur renvoyée par toString est immuable. En réalité, theDate est bel et bien exporté, indirectement :

 
Sélectionnez

Date d = new Date();
MaClasse c = new MaClasse(d);
d.setYear(98);
System.out.println(c);

Ce programme affiche Fri Mar 27 00:50:23 PST 1998, notre classe n'est donc pas immuable. Pour résoudre ce problème, il faut réaliser une copie défensive des objets non immuables passes en paramètre :

 
Sélectionnez

private final Date theDate;

public MaClasse(Date theDate) {
  this.theDate = (Date) theDate.clone();
}

Cette fois-ci le résultat est bien Mon Mar 27 00:52:18 PST 2006 malgré la modification de l'année. Ce qui est vrai pour les paramètres d'entrée de la classe l'est également pour les valeurs que la classe renvoie à travers ses différentes méthodes. Ainsi, pour ajouter une methode getDate() à notre classe, nous devrons écrire ceci :

 
Sélectionnez

public Date getDate() {
  return (Date) theDate.clone();
}

Malheureusement, la copie défensive ne suffit pas toujours. Prenez l'exemple suivant :

 
Sélectionnez

private final Date startDate;
private final Date endDate;

public MaClasse(Date startDate, Date endDate) {
  if (startDate.compareTo(endDate) > 0) {
    throw new IllegalArgumentException("The start date is not <= the end date.");
  }

  this.startDate = (Date) startDate.clone();
  this.endDate = (Date) endDate.clone();
}

Ce code semble parfait à première vue mais cache un gros problème potentiel. En effet, il est possible avec un thread de modifier startDate et/ou endDate de maniere à ce que la condition soit validée mais que des valeurs interdites soient conservées par l'objet. Puisqu'il n'est pas possible de prédire le séquencement des opérations multi-thread dans la plupart des environnements, rien ne garantit que le thread principal ne s'arrêtera pas juste après l'exécution du if pour donner la main à un second thread qui modifiera startYear pour que sa valeur soit supérieure a endYear. La bonne stratégie (la seule en fait) est la suivante :

 
Sélectionnez

private final Date startDate;
private final Date endDate;

public MaClasse(Date startDate, Date endDate) {
  Date copyStart = (Date) startDate.clone();
  Date copyEnd = (Date) endDate.clone()

  if (copyStart.compareTo(copyEnd) > 0) {
    throw new IllegalArgumentException("The start date is not <= the end date.");
  }

  this.startDate = copyStart;
  this.endDate = copyEnd;
}

Ce dernier exemple montre bien pourquoi les classes immuables sont souvent intéressantes. Si Date était immuable, nous pourrions simplement ecrire this.startDate = startDate.

Rendez-vous service, utilisez des classes immuables :)

Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).