Quand on examine la sérialisation d'objets dans Java (présentée au Chapitre
11), on se rend compte qu'un objet sérialisé puis désérialisé est, en fait, cloné.
Pourquoi alors
ne pas utiliser la sérialisation pour réaliser une copie profonde ? Voici un exemple qui
compare les deux approches en les chronométrant :
//: appendixa:Compete.java
import java.io.*;
class Thing1 implements Serializable {}
class Thing2 implements Serializable {
Thing1 o1 = new Thing1();
}
class Thing3 implements Cloneable {
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch(CloneNotSupportedException e) {
System.err.println("Thing3 can't clone");
}
return o;
}
}
class Thing4 implements Cloneable {
Thing3 o3 = new Thing3();
public Object clone() {
Thing4 o = null;
try {
o = (Thing4)super.clone();
} catch(CloneNotSupportedException e) {
System.err.println("Thing4 can't clone");
}
// Clone aussi la donnée membre :
o.o3 = (Thing3)o3.clone();
return o;
}
}
public class Compete {
static final int SIZE = 5000;
public static void main(String[] args)
throws Exception {
Thing2[] a = new Thing2[SIZE];
for(int i = 0; i < a.length; i++)
a[i] = new Thing2();
Thing4[] b = new Thing4[SIZE];
for(int i = 0; i < b.length; i++)
b[i] = new Thing4();
long t1 = System.currentTimeMillis();
ByteArrayOutputStream buf =
new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
for(int i = 0; i < a.length; i++)
o.writeObject(a[i]);
// Récupère les copies:
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(
buf.toByteArray()));
Thing2[] c = new Thing2[SIZE];
for(int i = 0; i < c.length; i++)
c[i] = (Thing2)in.readObject();
long t2 = System.currentTimeMillis();
System.out.println(
"Duplication via serialization: " +
(t2 - t1) + " Milliseconds");
// Maintenant on tente le clonage :
t1 = System.currentTimeMillis();
Thing4[] d = new Thing4[SIZE];
for(int i = 0; i < d.length; i++)
d[i] = (Thing4)b[i].clone();
t2 = System.currentTimeMillis();
System.out.println(
"Duplication via cloning: " +
(t2 - t1) + " Milliseconds");
}
} ///:~
Thing2 et Thing4 contiennent des objets
membres afin qu'une copie profonde soit nécessaire. Il est intéressant de noter que bien que les
classes Serializable soient plus faciles à implémenter, elles nécessitent plus de
travail pour les copier. Le support du clonage demande plus de travail pour créer la classe, mais
la duplication des objets est relativement simple. Les résultats sont édifiants. Voici la sortie
obtenue pour trois exécutions :
Duplication via serialization: 940 Milliseconds
Duplication via cloning: 50 Milliseconds
Duplication via serialization: 710 Milliseconds
Duplication via cloning: 60 Milliseconds
Duplication via serialization: 770 Milliseconds
Duplication via cloning: 50 Milliseconds
Outre la différence significative de temps entre la sérialisation et le
clonage, vous noterez aussi que la sérialisation semble beaucoup plus sujette aux variations,
tandis que le clonage a tendance à être plus stable.
Supporter le clonage plus bas dans la hiérarchie
Si une nouvelle classe est créée, sa classe de base par défaut est
Object, qui par défaut n'est pas cloneable (comme vous le verrez dans la section
suivante). Tant qu'on n'implémente pas explicitement le clonage, celui-ci ne sera pas
disponible. Mais on peut le rajouter à n'importe quel niveau et la classe sera cloneable à
partir de ce niveau dans la hiérarchie, comme ceci :
//: appendixa:HorrorFlick.java
// On peut implémenter le Clonage
// à n'importe quel niveau de la hiérarchie.
import java.util.*;
class Person {}
class Hero extends Person {}
class Scientist extends Person
implements Cloneable {
public Object clone() {
try {
return super.clone();
} catch(CloneNotSupportedException e) {
// Ceci ne devrait jamais arriver :
// la classe est Cloneable !
throw new InternalError();
}
}
}
class MadScientist extends Scientist {}
public class HorrorFlick {
public static void main(String[] args) {
Person p = new Person();
Hero h = new Hero();
Scientist s = new Scientist();
MadScientist m = new MadScientist();
// p = (Person)p.clone(); // Erreur lors de la compilation
// h = (Hero)h.clone(); // Erreur lors de la compilation
s = (Scientist)s.clone();
m = (MadScientist)m.clone();
}
} ///:~
Tant que le clonage n'est pas supporté, le compilateur bloque toute
tentative de clonage. Si le clonage est ajouté dans la classe Scientist, alors
Scientist et tous ses descendants sont cloneables.
Pourquoi cet étrange design ?
Si tout ceci vous semble étrange, c'est parce que ça l'est réellement. On
peut se demander comment on en est arrivé là. Que se cache-t-il derrière cette conception
?
Originellement, Java a été conçu pour piloter des boîtiers, sans aucune
pensée pour l'Internet. Dans un langage générique tel que celui-ci, il semblait sensé que le
programmeur soit capable de cloner n'importe quel objet. C'est ainsi que clone() a
été placée dans la classe de base Object, mais c'était une méthode
public afin qu'un objet puisse toujours être cloné. Cela semblait l'approche la
plus flexible, et après tout, quel mal y avait-il à cela ?
Puis, quand Java s'est révélé comme le langage de programmation idéal pour
Internet, les choses ont changé. Subitement, des problèmes de sécurité sont apparus, et bien sûr,
ces problèmes ont été réglés en utilisant des objets, et on ne voulait pas que n'importe qui soit
capable de cloner ces objets de sécurité. Ce qu'on voit donc est une suite de patchs appliqués sur
l'arrangement initialement simple : clone() est maintenant
protected dans Object. Il faut la redéfinir et
implementer Cloneableet traiter les
exceptions.
Il est bon de noter qu'on n'est obligé d'utiliser l'interface
Cloneable que si on fait un appel à la méthode clone()
de Object, puisque cette méthode vérifie lors de l'exécution que la classe
implémente Cloneable. Mais dans un souci de cohérence (et puisque de toute façon
Cloneable est vide), il vaut mieux l'implémenter.
Contrôler la clonabilité
On pourrait penser que, pour supprimer le support du clonage, il
suffit de rendre private la méthode clone(), mais ceci ne
marchera pas car on ne peut prendre une méthode de la classe de base et la rendre moins accessible
dans une classe dérivée. Ce n'est donc pas si simple. Et pourtant, il est essentiel d'être capable
de contrôler si un objet peut être cloné ou non. Une classe peut adopter plusieurs attitudes à ce
propos :
- L'indifférence. Rien n'est fait pour supporter le clonage, ce qui signifie
que la classe ne peut être clonée, mais qu'une classe dérivée peut implémenter le clonage si elle
veut. Ceci ne fonctionne que si Object.clone() traite comme il faut tous les
champs de la classe.
- Implémenter clone(). Respecter la marche à suivre pour
l'implémentation de Cloneable et redéfinir clone(). Dans la
méthode clone() redéfinie, appeler super.clone() et intercepter
toutes les exceptions (afin que la méthode clone() redéfinie ne génère pas
d'exceptions).
- Supporter le clonage conditionnellement. Si la classe contient des
références sur d'autres objets qui peuvent ou non être cloneables (une classe conteneur, par
exemple), la méthode clone() peut essayer de cloner tous les objets référencés, et
s'ils génèrent des exceptions, relayer ces exceptions au programmeur. Par exemple, prenons le cas
d'une sorte d'ArrayList qui essayerait de cloner tous les objets qu'elle contient.
Quand on écrit une telle ArrayList, on ne peut savoir quelle sorte d'objets le
programmeur client va pouvoir stocker dans l'ArrayList, on ne sait donc pas s'ils
peuvent être clonés.
- Ne pas implémenter Cloneable mais redéfinir
clone() en protected, en s'assurant du fonctionnement correct du
clonage pour chacun des champs. De cette manière, toute classe dérivée peut redéfinir
clone() et appeler super.clone() pour obtenir le comportement
attendu lors du clonage. Cette implémentation peut et doit invoquer super.clone()
même si cette méthode attend un objet Cloneable (elle génère une exception sinon),
car personne ne l'invoquera directement sur un objet de la classe. Elle ne sera invoquée qu'à
travers une classe dérivée, qui, elle, implémente Cloneable si elle veut obtenir
le fonctionnement désiré.
- Tenter de bloquer le clonage en n'implémentant pas
Cloneable et en redéfinissant clone() afin de générer une
exception. Ceci ne fonctionne que si toutes les classes dérivées appellent
super.clone() dans leur redéfinition de clone(). Autrement, un
programmeur est capable de contourner ce mécanisme.
- Empêcher le clonage en rendant la classe final. Si
clone() n'a pas été redéfinie par l'une des classes parentes, alors elle ne peut
plus l'être. Si elle a déjà été redéfinie, la redéfinir à nouveau et générer une exception
CloneNotSupportedException. Rendre la classe final est la seule
façon d'interdire catégoriquement le clonage. De plus, si on manipule des objets de sécurité ou
dans d'autres situations dans lesquelles on veut contrôler le nombre d'objets créés, il faut rendre
tous les constructeurs private et fournir une ou plusieurs méthodes spéciales pour
créer les objets. De cette manière, les méthodes peuvent restreindre le nombre d'objets créés et
les conditions dans lesquelles ils sont créés (un cas particulier en est le patron
singleton présenté dans Thinking in Patterns with Java, téléchargeable à
www.BruceEckel.com).
Voici un exemple qui montre les différentes façons dont le clonage peut
être implémenté et interdit plus bas dans la hiérarchie :
//: appendixa:CheckCloneable.java
// Vérifie si une référence peut être clonée.
// Ne peut être clonée car ne redéfinit pas clone() :
class Ordinary {}
// Redéfinit clone, mais n'implémente pas
// Cloneable :
class WrongClone extends Ordinary {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Génère une exception
}
}
// Fait le nécessaire pour le clonage :
class IsCloneable extends Ordinary
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Interdit le clonage en générant une exception :
class NoMore extends IsCloneable {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone()
throws CloneNotSupportedException {
// Appelle NoMore.clone(), génère une excpetion :
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Crée une copie de b d'une façon ou d'une autre
// et renvoie cette copie. C'est une copie sans
// intérêt, juste pour l'exemple :
return new BackOn();
}
public Object clone() {
// N'appelle pas NoMore.clone() :
return duplicate(this);
}
}
// On ne peut dériver cette classe, donc on ne peut
// redéfinir la méthode clone comme dans BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable {
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if(ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch(CloneNotSupportedException e) {
System.err.println("Could not clone "+id);
}
}
return x;
}
public static void main(String[] args) {
// Transtypage ascendant :
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};
Ordinary x = new Ordinary();
// Ceci ne compilera pas, puisque clone()
// est protected dans Object:
//! x = (Ordinary)x.clone();
// tryToClone() vérifie d'abord si
// une classe implémente Cloneable :
for(int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
} ///:~
La première classe, Ordinary, représente le genre de
classes que nous avons rencontré tout au long de ce livre : pas de support du clonage, mais
pas de contrôle sur la clonabilité non plus. Mais si on dispose d'une référence sur un objet
Ordinary qui peut avoir été transtypé à partir d'une classe dérivée, on ne peut
savoir s'il est peut être cloné ou non.
La classe WrongClone montre une implémentation incorrecte
du clonage. Elle redéfinit bien Object.clone() et rend la méthode
public, mais elle n'implémente pas Cloneable, donc quand
super.clone() est appelée (ce qui revient à un appel à
Object.clone()), une exception CloneNotSupportedException est
générée et le clonage échoue.
La classe IsCloneable effectue toutes les actions
nécessaires au clonage : clone() est redéfinie et Cloneable
implémentée. Cependant, cette méthode clone() et plusieurs autres qui suivent dans
cet exemple n'interceptent pasCloneNotSupportedException, mais la font
suivre à l'appelant, qui doit alors l'envelopper dans un bloc try-catch. Dans les méthodes
clone() typiques il faut intercepter CloneNotSupportedException
à l'intérieur de clone() plutôt que de la propager. Cependant dans cet
exemple, il est plus intéressant de propager les exceptions.
La classe NoMore tente d'interdire le clonage comme les
concepteurs de Java pensaient le faire : en générant une exception
CloneNotSupportedException dans la méthode clone() de la classe
dérivée. La méthode clone() de la classe TryMore appelle
super.clone(), ce qui revient à appeler NoMore.clone(), qui
génère une exception et empêche donc le clonage.
Mais que se passe-t-il si le programmeur ne respecte pas la chaîne d'appel
« recommandée » et n'appelle pas super.clone() à l'intérieur de la
méthode clone() redéfinie ? C'est ce qui se passe dans la classe
BackOn. Cette classe utilise une méthode séparée duplicate() pour
créer une copie de l'objet courant et appelle cette méthode dans clone()au
lieu d'appeler super.clone(). L'exception n'est donc jamais générée et la
nouvelle classe est cloneable. La seule solution vraiment sûre est montrée dans
ReallyNoMore, qui est final et ne peut donc être dérivée. Ce qui
signifie que si clone() génère une exception dans la classe
final, elle ne peut être modifiée via l'héritage et la prévention du clonage est
assurée (on ne peut appeler explicitement Object.clone() depuis une classe qui a
un niveau arbitraire d'héritage ; on en est limité à appeler super.clone(),
qui a seulement accès à sa classe parente directe). Implémenter des objets qui traite de sujets
relatifs à la sécurité implique donc de rendre ces classes final.
La première méthode qu'on voit dans la classe
CheckCloneable est tryToClone(), qui prend n'importe quel objet
Ordinary et vérifie s'il est cloneable grâce à instanceof. Si
c'est le cas, il transtype l'objet en IsCloneable, appelle
clone() et retranstype le résultat en Ordinary, interceptant
toutes les exceptions générées. Remarquez l'utilisation de l'identification dynamique du type (voir
Chapitre 12) pour imprimer le nom de la classe afin de suivre le déroulement du
programme.
Dans main(), différents types d'objets Ordinary sont créés et
transtypés en Ordinary dans la définition du tableau. Les deux premières lignes de
code qui suivent créent un objet Ordinary et tentent de le cloner. Cependant ce
code ne compile pas car clone() est une méthode protected dans
Object. Le reste du code parcourt le tableau et essaye de cloner chaque objet,
reportant le succès ou l'échec de l'opération. Le résultat est :
Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore