En Java nous avons la possibilité d’utiliser des objets immutables. Il s’agit d’objets, qui une fois initialisés, ne peuvent plus être modifiés. L’intérêt est notamment d’avoir des objets qui sont par définition thread safe, ce qui permet d’éviter d’avoir des blocs synchronized
autour de ces objets. Dans cet article nous voyons comment créer ces objets, quand les utiliser ainsi que leurs avantages et inconvénients.
Débloque les + belles offres tech en 10 mins
Créer un objet vraiment immutable est plus complexe qu’il n’y paraît. La première chose concerne la définition des variables membres. A ce niveau c’est très simple : elles doivent toutes être déclarées final
, ce qui oblige à ce qu’elle soient affectées lors de l’initialisation d’une instance de l’objet. L’avantage est que la JVM traite ces variables d’instance spécialement, et donc là encore on évite la nécessité d’un bloc synchronized
si tant est que pour une raison ou l’autre c’était nécessaire.
De même la classe immutable doit être final
car sinon des classes filles mutables pourraient en hériter, ce qui pourrait poser problème.
Ca c’était la partie facile. Maintenant considérons le code suivant :
MonObjImmutable obj = ... ;
// ...
List<String> l = obj.getList();
l.add("hello");
On observe qu’ici même si la liste de MonObjImmutable
était settée lors de la construction, on peut la modifier par la suite. Dès lors il convient de s’assurer que ce genre d’opération n’est pas possible. Deux solutions ici : soit vous encapsulez les variables internes de votre objet immutable dans des conteneurs immutables comme Collections.unmodifiableList()
, soit si ce n’est vraiment pas possible vous faites une copie en profondeur des variables membres. Pour ce dernier cas on peut considérer le code suivant :
MonObjImmutable obj = ... ;
// ...
List<Date> l = obj.getList();
for (Date d: l) {
d.setTime(42L);
}
La nécessité ici que la méthode getList()
retourne des copies des objets de la liste originale de obj
en lieu et place de références d’objets de cette même liste paraît ici évidente.
Dernier point, lors de la construction, pensez à faire de la copie défensive. Je m’explique avec le code suivant :
List<String> l = new ArrayList<>();
// ...
MonObjImmutable obj = new MonObjImmutable(l);
l.add("hello");
Si ici il n’y a pas de copie défensive on peut parfaitement modifier le contenu de l’instance obj
après que celle-ci soit initialisée, autrement dit on casse le contrat d’immutabilité.
Comme on l’a vu les objets immutables sont de très bons candidats pour avoir des variables thread-safe sans efforts. Ils sont aussi nécessaires pour définir des constantes. Considérons à ce sujet le code suivant :
private final static String[] CONSTANT_ATTEMPT = {"one", "two", "three"};
Ce code n’est pas safe car on peut modifier le contenu de CONSTANT_ATTEMPT
. Vous ne me croyez pas ? Essayez donc ceci :
CONSTANT_ATTEMPT[2] = "four";
La version safe de ce code serait :
private final static List<String> CONSTANT = Collections.unmodifiableList(Arrays.asList("one", "two", "three"));
Les Set
et les Map
utilisent des discriminants pour accélérer la recherche. Dans le cas des Hash-collections, la méthode appelée va être hashCode
, ce qui explique pourquoi on redéfinit toujours ce dernier en même temps que le equals
. Dans le cas des Sorted-collections on utilise le compareTo
. Dans les deux cas ce discriminant ne doit pas changer au cours du temps, sinon vous cassez le fonctionnement de la Map qui le contient. Pas convaincu ? Essayez donc le code ci-dessous :
Set<Date> set = new HashSet<>();
Date d = new Date(100000000L);
set.add(d);
System.out.println(set.contains(d)); // affiche true
d.setTime(50000L);
System.out.println(set.contains(d)); // affiche très très probablement false, dans 99.999999% des cas
La même chose pourrait s’appliquer si vous utilisiez une Map
. Pour info ce point est écrit noir sur blanc dans la documentation de l’interface Map, et comme un Set
n’est qu’en fait un wrapper pour la Map
correspondante dans la plupart des JVM, le comportement du Set
est analogue à celui d’une Map
sur ce point…
Les objets immutables ont plein d’avantages, mais présentent un gros défaut. Par leur nature si vous voulez une instance modifiée d’un objet immutable vous êtes obligé d’en créer une copie. Ceci peut à terme augmenter le nombre de cycles de garbage collector sur votre JVM, ce qui a dans certains cas un impact non négligeable sur les performances.
Débloque les + belles offres tech en 10 mins
Cet article vous a plu ? Vous aimerez sûrement aussi :
Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.
Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.
Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…
Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…
On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…
Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…
Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…
View Comments
Salut,
Ton exemple avec le static List CONSTANT est faux puisqu'on peut également le modifier : List n'est pas immuable !!!
Exemple : CONSTANT.set(0, "four");
Il faut utiliser Collections.unmodifiableList() pour cela !!!
Sinon quelques remarques :
- Il est préférable que les attributs soit déclarées final, mais ce n'est pas une obligation. On peut très bien utiliser des attributs non-finaux tant que cela n'a aucun impact sur l'état de l'objet (c'est par exemple le cas de la classe String).
- Par contre il est impérative de rendre la classe final (ou de contrôler ses classes filles avec un constructeur private). En effet le simple fait de pouvoir créer une classe fille peut permettre de "casser" l'immuabilité du type.
- Enfin je ne suis pas d'accord concernant l'usage mémoire. Au contraire l'immuabilité facilite l'utilisation des objets et évites toutes les multiples copies défensives... Sauf utilisation incorrecte, il y a de forte chance que cela diminue le nombre d'instance...
a++
Bien vu pour Arrays.asList(). En fait j'appliquais le conseil de findbugs, mais visiblement il y a un bug dessus.
Pour les attributs final, ça compte lorsque l'objet est partagé entre plusieurs threads. Le fait de mettre les attributs en final permet de s'assurer que l'initialisation de l'objet est thread-safe et que d'autres threads ne "verront" pas l'objet à moitié initialisé. La classse String a son tableau de caractères interne en final mais pas le hash, qui lui est mis en cache à la première invocation de hashCode mais ne modifie pas l'état de l'objet face aux méthodes equals() et hashCode().
Effectivement également par héritage on peut casser l'immutabilité... des attributs de la classe fille, mais pas ceux de la classe mère.
Enfin oui les objets immutables évitent les copies défensives. Le problème va par exemple porter sur toutes les manipulations de modifications de String. On remplit vite l'Eden avec ça avec des GC (non full le plus souvent, heureusement).
A moins de bidouiller avec des threads dans le constructeur, les attributs seront bien initialisé sans problème avec ou sans final.
Utiliser des attributs final c'est une bonne pratique en général, et c'est conseillé pour des classes immuables, mais ce n'est pas une obligation.
L'héritage casse l'immuabilité de la classe mère : tu ne peux plus la considérer comme immuable, car on peut associer une instance de la classe fille (muable) à une référence de la classe parente (qu'on croit immuable).
Enfin en toute logique les objets immuables ne sont pas conçus pour les "manipulations de modifications". C'est pour cela qu'on utilise des classes muables comme StringBuilder.
Mais essayes de remplacer les classes immuables (String,Integer,...) par des version muables... et tu verras que l'utilisation mémoire va augmenter (tout comme les bugs).
a++