Penser en Java 2nde édition | - | Sommaire | Préface | Avant-propos | Chapitre : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Annexe : A B C D | Tables des matières | - | Thinking in Java |
Ceci produira l'effet désiré.
Le clonage peut sembler un processus compliqué à mettre en oeuvre. On se dit qu'il doit certainement exister une autre alternative, et souvent on envisage (surtout les programmeurs C++) de créer un constructeur spécial dont le travail est de dupliquer un objet. En C++, on l'appelle le constructeur de copie. Cela semble a priori la solution la plus évidente, mais en fait elle ne fonctionne pas. Voici un exemple.
Ceci semble un peu étrange à première vue. Bien sûr, un fruit a des qualités, mais pourquoi ne pas mettre les données membres représentant ces qualités directement dans la classe Fruit ? Deux raisons à cela. La première est qu'on veut pouvoir facilement insérer ou changer les qualités. Notez que Fruit possède une méthode protected addQualities() qui permet aux classes dérivées de le faire (on pourrait croire que la démarche logique serait d'avoir un constructeur protected dans Fruit qui accepte un argument FruitQualities, mais les constructeurs ne sont pas hérités et ne seraient pas disponibles dans les classes dérivées). En créant une classe séparée pour la qualité des fruits, on dispose d'une plus grande flexibilité, incluant la possibilité de changer les qualités d'un objet Fruit pendant sa durée de vie.
La deuxième raison pour laquelle on a décidé de créer une classe FruitQualities est dans le cas où on veut ajouter de nouvelles qualités ou en changer le comportement via héritage ou polymorphisme. Notez que pour les GreenZebra (qui sont réellement un type de tomates - j'en ai cultivé et elles sont fabuleuses), le constructeur appelle addQualities() et lui passe un objet ZebraQualities, qui est dérivé de FruitQualities et peut donc être attaché à la référence FruitQualities de la classe de base. Bien sûr, quand GreenZebra utilise les FruitQualities il doit le transtyper dans le type correct (comme dans evaluate()), mais il sait que le type est toujours ZebraQualities.
Vous noterez aussi qu'il existe une classe Seed, et qu'un Fruit (qui par définition porte ses propres graines) name="fnB82">[82]contient un tableau de Seeds.
Enfin, vous noterez que chaque classe dispose d'un constructeur de copie, et que chaque constructeur de copie doit s'occuper d'appeler le constructeur de copie de la classe de base et des objets membres pour réaliser une copie profonde. Le constructeur de copie est testé dans la classe CopyConstructor. La méthode ripen() accepte un argument Tomato et réalise une construction de copie afin de dupliquer l'objet :
tandis que slice( ) accepte un objet Fruit plus générique et le duplique aussi :
Ces deux méthodes sont testées avec différents types de Fruit dans main(). Voici la sortie produite :
C'est là que le problème survient. Après la construction de copie réalisée dans slice() sur l'objet Tomato, l'objet résultant n'est plus un objet Tomato, mais seulement un Fruit. Il a perdu toute sa tomaticité. De même, quand on prend une GreenZebra, ripen() et slice() la transforment toutes les deux en Tomato et Fruit, respectivement. La technique du constructeur de copie ne fonctionne donc pas en Java pour créer une copie locale d'un objet.
Le constructeur de copie est un mécanisme fondamental en C++, puisqu'il permet de créer automatiquement une copie locale d'un objet. Mais l'exemple précédent prouve que cela ne fonctionne pas en Java. Pourquoi ? En Java, toutes les entités manipulées sont des références, tandis qu'en C++ on peut manipuler soit des références sur les objets soit les objets directement. C'est le rôle du constructeur de copie en C++ : prendre un objet et permettre son passage par valeur, donc dupliquer l'objet. Cela fonctionne donc très bien en C++, mais il faut garder présent à l'esprit que ce mécanisme est à proscrire en Java.
Bien que la copie locale produite par clone() donne les résultats escomptés dans les cas appropriés, c'est un exemple où le programmeur (l'auteur de la méthode) est responsable des effets secondaires indésirables de l'aliasing. Que se passe-t-il dans le cas où on construit une bibliothèque tellement générique et utilisée qu'on ne peut supposer qu'elle sera toujours clonée aux bons endroits ? Ou alors, que se passe-t-il si on veut permettre l'aliasing dans un souci d'efficacité - afin de prévenir la duplication inutile d'un objet - mais qu'on n'en veut pas les effets secondaires négatifs ?
Une solution est de créer des objets immuables appartenant à des classes en lecture seule. On peut définir une classe telle qu'aucune méthode de la classe ne modifie l'état interne de l'objet. Dans une telle classe, l'aliasing n'a aucun impact puisqu'on peut seulement lire son état interne, donc même si plusieurs portions de code utilisent le même objet cela ne pose pas de problèmes.
Par exemple, la bibliothèque standard Java contient des classes name="Index2266">« wrapper » pour tous les types fondamentaux. Vous avez peut-être déjà découvert que si on veut stocker un int dans un conteneur tel qu'une ArrayList (qui n'accepte que des références sur un Object), on peut insérer l'int dans la classe Integer de la bibliothèque standard :
La classe Integer (de même que toutes les classes « wrapper » pour les scalaires) implémentent l'immuabilité d'une manière simple : elles ne possèdent pas de méthodes qui permettent de modifier l'objet.
Si on a besoin d'un objet qui contient un scalaire qui peut être modifié, il faut la créer soi-même. Heureusement, ceci se fait facilement :
Notez que n est amical pour simplifier le codage.
IntValue peut même être encore plus simple si l'initialisation à zéro est acceptable (auquel cas on n'a plus besoin du constructeur) et qu'on n'a pas besoin d'imprimer cet objet (auquel cas on n'a pas besoin de toString()) :
La recherche de l'élément et son transtypage par la suite est un peu lourd et maladroit, mais c'est une particularité de ArrayList et non de IntValue.
Il est possible de créer ses propres classes en lecture seule. Voici un exemple :
Toutes les données sont private, et aucune méthode public ne modifie les données. En effet, la méthode qui semble modifier l'objet, quadruple(), crée en fait un nouvel objet Immutable1 sans modifier l'objet original.
La méthode f() accepte un objet Immutable1 et effectue diverses opérations avec, et la sortie de main() démontre que x ne subit aucun changement. Ainsi, l'objet x peut être aliasé autant qu'on le veut sans risque puisque la classe Immutable1 a été conçue afin de guarantir que les objets ne puissent être modifiés.
Créer une classe immuable semble à première vue une solution élégante. Cependant, dès qu'on a besoin de modifier un objet de ce nouveau type, il faut supporter le coût supplémentaire de la création d'un nouvel objet, ce qui implique aussi un passage plus fréquent du ramasse-miettes. Cela n'est pas un problème pour certaines classes, mais cela est trop coûteux pour certaines autres (telles que la classes String).
La solution est de créer une classe compagnon qui, elle, peut être modifiée. Ainsi, quand on effectue beaucoup de modifications, on peut basculer sur la classe compagnon modifiable et revenir à la classe immuable une fois qu'on en a terminé.
L'exemple ci-dessus peut être modifié pour montrer ce mécanisme :
Immutable2 contient des méthodes qui, comme précédemment, préservent l'immuabilité des objets en créant de nouveaux objets dès qu'une modification est demandée. Ce sont les méthodes add() et multiply(). La classe compagnon est appelée Mutable, et possède aussi des méthodes add() et multiply(), mais ces méthodes modifient l'objet Mutable au lieu d'en créer un nouveau. De plus, Mutable possède une méthode qui utilise ses données pour créer un objet Immutable2 et vice-versa.
Les deux méthodes static modify1() et modify2() montrent deux approches différentes pour arriver au même résultat. Dans modify1(), tout est réalisé dans la classe Immutable2 et donc quatre nouveaux objets Immutable2 sont créés au cours du processus (et chaque fois que val est réassignée, l'instance précédente est récupérée par le ramasse-miettes).
Dans la méthode modify2(), on peut voir que la première action réalisée est de prendre l'objet Immutable2 y et d'en produire une forme Mutable (c'est comme si on appelait clone() vue précédemment, mais cette fois un différent type d'objet est créé). L'objet Mutable est alors utilisé pour réaliser un grand nombre d'opérations sans nécessiter la création de nombreux objets. Puis il est retransformé en objet Immutable2. On n'a donc créé que deux nouveaux objets (l'objet Mutable et le résultat Immutable2) au lieu de quatre.