Clonage d'objets Java
Copiez des objets Java avec clone(), l'interface Cloneable, et comprenez la différence entre copie superficielle et copie profonde.
Cloner un objet signifie produire un nouvel objet avec le même état — une copie que vous pouvez modifier indépendamment de l'original. La réponse intégrée de Java est Object.clone() associée à l'interface marqueur Cloneable, mais la conception présente suffisamment d'aspérités que la plupart du code moderne préfère un constructeur de copie ou une méthode factory. Ce chapitre présente les deux approches et le piège qui existe entre elles.
La voie intégrée : Object.clone()
Object.clone() est protected et effectue une copie superficielle champ par champ de l'instance. Pour l'utiliser, vous devez :
- Faire implémenter
Cloneableà votre classe — une interface marqueur sans méthode. Sans elle,clone()lèveCloneNotSupportedException. - Surcharger
clone()pour le rendrepublicet (généralement) réduire le type de retour à la classe réelle.
public class Box implements Cloneable {
int size;
@Override
public Box clone() {
try {
return (Box) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // can't happen — we implement Cloneable
}
}
}super.clone() produit la copie réelle. Le try/catch est une formalité administrative : l'exception vérifiée est déclarée sur Object.clone, mais notre classe implémente Cloneable, donc l'exception est inaccessible.
Copie superficielle : ce qu'elle fait réellement
Une copie superficielle duplique les champs immédiats. Les références à l'intérieur de l'objet sont copiées en tant que références, pas en tant que nouveaux objets — ainsi l'original et le clone partagent ce vers quoi ces références pointent :
public class Person implements Cloneable {
String name;
int[] scores;
@Override
public Person clone() {
try { return (Person) super.clone(); }
catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
Person a = new Person();
a.scores = new int[]{1, 2, 3};
Person b = a.clone();
b.scores[0] = 99;
System.out.println(a.scores[0]); // 99 — they share the same arrayPour les primitives et les valeurs immuables (String, Integer, LocalDate), la copie superficielle convient. Pour les sous-objets mutables, elle est presque toujours incorrecte — muter le clone affecte également l'original.
Copie profonde : la solution
Pour obtenir une copie véritablement indépendante, surchargez clone() pour copier récursivement les champs mutables :
@Override
public Person clone() {
try {
Person copy = (Person) super.clone();
copy.scores = scores.clone(); // arrays have their own clone()
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}Les tableaux implémentent Cloneable nativement et se copient avec .clone(). Les collections, non — pour un champ List<String> vous écririez copy.items = new ArrayList<>(items); (voir ArrayList). Pour un graphe de vos propres types mutables, chaque type dans le graphe doit participer.
Pourquoi Cloneable a mauvaise réputation
Quelques particularités rendent clone() peu commode :
- C'est une interface marqueur sans méthode
clone()—Cloneableelle-même n'expose rien, le contrat réside surObject.clone(). - Elle contourne les constructeurs — les champs du nouvel objet sont remplis par la JVM, donc les invariants que vous appliquez dans votre constructeur ne sont pas re-vérifiés.
- Les sous-classes héritent de l'obligation : si
Parentsurchargeclone(), chaque sous-classe doit maintenir la logique de copie profonde synchronisée, ou hériter silencieusement d'une version superficielle défectueuse. - L'exception vérifiée, le cast, l'appel à
super.clone()— chaque surcharge répète le même bruit.
L'alternative moderne : le constructeur de copie
Un constructeur de copie est simplement un constructeur qui prend une instance de la même classe et copie ses champs :
public class Person {
String name;
int[] scores;
public Person(Person other) {
this.name = other.name;
this.scores = other.scores.clone(); // deep where it matters
}
}
Person b = new Person(a);Il passe par le constructeur normal, donc les invariants sont vérifiés. C'est du Java simple — pas d'interface marqueur, pas de CloneNotSupportedException, pas de cast. Les sous-classes écrivent simplement leur propre constructeur de copie qui appelle super(other). La recommandation d'Effective Java est de préférer les constructeurs de copie (ou les factories statiques copyOf) à clone.
Les classes de type collection suivent déjà ce pattern : new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).
Quelle approche dois-je utiliser ?
| Situation | Approche recommandée |
|---|---|
| Nouvelle classe simple que vous contrôlez | Constructeur de copie ou factory statique copyOf |
| Classe avec seulement des champs primitifs / immuables | N'importe laquelle — même un clone() superficiel est sûr |
| Classe avec des champs mutables (listes, tableaux, objets imbriqués) | Constructeur de copie avec copies profondes explicites |
API existante qui requiert déjà Cloneable | Surcharger clone() et copier en profondeur les champs mutables |
| Type valeur que vous pouvez reconcevoir | Rendez-le immuable — aucune copie n'est alors nécessaire |
En résumé : préférez un constructeur de copie pour le nouveau code, et n'utilisez clone() que lorsqu'un contrat existant vous y oblige.
Records et types immuables
Les Records sont immuables, donc ils n'ont pas besoin de clonage — partagez la même référence partout. Si vous avez besoin d'une copie modifiée, écrivez de petites méthodes with... :
record Point(int x, int y) {
Point withX(int newX) { return new Point(newX, y); }
}Ce style — « construire une nouvelle instance avec un champ modifié » — est généralement plus clair que le clonage suivi d'une mutation.
Un exemple complet
Et ensuite
La plupart des problèmes liés au clonage disparaissent si la classe est immuable dès le départ — rien à copier défensivement, pas de surprises d'aliasing, sans danger pour le partage entre threads. Le prochain chapitre explique ce qu'il faut pour concevoir une classe de cette façon. Continuez vers les classes immuables Java.