Clonazione di Oggetti in Java
Copia oggetti Java con clone(), l'interfaccia Cloneable e le differenze tra copia superficiale e copia profonda.
Clonare un oggetto significa produrre un nuovo oggetto con lo stesso stato — una copia che puoi modificare indipendentemente dall'originale. La risposta integrata di Java è Object.clone() insieme all'interfaccia marcatore Cloneable, ma il design presenta abbastanza irregolarità che la maggior parte del codice moderno preferisce un copy constructor o un factory method. Questo capitolo mostra entrambe le strade e la trappola che si nasconde tra di esse.
La via integrata: Object.clone()
Object.clone() è protected e produce una copia superficiale campo per campo dell'istanza. Per usarla devi:
- Far implementare alla tua classe
Cloneable— un'interfaccia marcatore senza metodi. Senza di essa,clone()lanciaCloneNotSupportedException. - Eseguire l'override di
clone()per renderlopublice (di solito) restringere il tipo di ritorno alla classe effettiva.
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() produce la copia effettiva. Il blocco try/catch è burocrazia: l'eccezione checked è dichiarata su Object.clone, ma la nostra classe implementa Cloneable, quindi l'eccezione è irraggiungibile.
Copia superficiale: cosa fa davvero
Una copia superficiale duplica i campi immediati. I riferimenti all'interno dell'oggetto vengono copiati come riferimenti, non come nuovi oggetti — quindi l'originale e il clone condividono tutto ciò a cui puntano quei riferimenti:
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 arrayPer i primitivi e i valori immutabili (String, Integer, LocalDate), la copia superficiale va bene. Per i sotto-oggetti mutabili, è quasi sempre sbagliata — modificare il clone si ripercuote sull'originale.
Copia profonda: la soluzione
Per ottenere una copia veramente indipendente, esegui l'override di clone() per copiare ricorsivamente i campi mutabili:
@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);
}
}Gli array implementano Cloneable nativamente e si copiano con .clone(). Le Collection no — per un campo List<String> scriveresti copy.items = new ArrayList<>(items); (vedi ArrayList). Per un grafo di tuoi tipi mutabili, ogni tipo nel grafo deve partecipare.
Perché Cloneable ha una cattiva reputazione
Alcune stranezze rendono clone() scomodo:
- È un'interfaccia marcatore senza metodo
clone()—Cloneabledi per sé non espone nulla; il contratto risiede suObject.clone(). - Bypassa i costruttori — i campi del nuovo oggetto vengono riempiti dalla JVM, quindi eventuali invarianti che applichi nel tuo costruttore non vengono riverificati.
- Le sottoclassi ereditano l'obbligo: se
Parentfa l'override diclone(), ogni sottoclasse deve mantenere sincronizzata la logica di copia profonda, oppure eredita silenziosamente una versione superficiale non funzionante. - L'eccezione checked, il cast, la chiamata
super.clone()— ogni override ripete lo stesso rumore.
L'alternativa moderna: copy constructor
Un copy constructor è semplicemente un costruttore che accetta un'istanza della stessa classe e ne copia i campi:
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);Viene eseguito attraverso il normale costruttore, quindi gli invarianti vengono verificati. È Java puro — nessuna interfaccia marcatore, nessuna CloneNotSupportedException, nessun cast. Le sottoclassi scrivono semplicemente il proprio copy constructor che chiama super(other). La raccomandazione di Effective Java è di preferire i copy constructor (o le factory statiche copyOf) rispetto a clone.
Le classi simili alle Collection seguono già questo pattern: new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).
Quale approccio dovrei usare?
| Situazione | Approccio consigliato |
|---|---|
| Classe nuova e semplice che controlli tu | Copy constructor o factory statica copyOf |
| Classe con soli campi primitivi o immutabili | Qualsiasi — anche un clone() superficiale è sicuro |
| Classe con campi mutabili (liste, array, oggetti annidati) | Copy constructor con copie profonde esplicite |
API esistente che richiede già Cloneable | Fai l'override di clone() e copia in profondità i campi mutabili |
| Tipo valore che puoi riprogettare | Rendilo immutabile — così nessuna copia è necessaria |
In breve: preferisci un copy constructor per il codice nuovo, e ricorri a clone() solo quando un contratto esistente ti obbliga.
Record e tipi immutabili
I Record sono immutabili, quindi non hanno bisogno di clonazione — condividi lo stesso riferimento ovunque. Se hai bisogno di una copia modificata, scrivi piccoli metodi with...:
record Point(int x, int y) {
Point withX(int newX) { return new Point(newX, y); }
}Questo stile — "costruisci una nuova istanza con un campo cambiato" — è di solito più chiaro della clonazione seguita da mutazione.
Un esempio pratico
Cosa c'è dopo
La maggior parte dei problemi legati alla clonazione scompare se la classe è immutabile fin dall'inizio — niente da copiare in modo difensivo, nessuna sorpresa di aliasing, sicura da condividere tra thread. Il prossimo capitolo illustra come progettare una classe in questo modo. Continua con Classi immutabili in Java.