Classes et objets immuables

Romain Guy (Gfx)

- Titre : Classes et objets immuables

- Auteur : Romain Guy (Gfx)

- Parution : 31 juillet 2006

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

- I. Introduction ♪

- II. Créer une classe immuable

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 par 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 :

private final Date theDate;

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

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

À 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 :

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 :

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 méthode getDate() à notre classe, nous devrons écrire ceci :

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

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

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 manière à 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 multithread 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 :

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 écrire this.startDate = startDate.

Rendez-vous service, utilisez des classes immuables :)