Clonación de objetos en Java
Copia objetos Java con clone(), la interfaz Cloneable y las diferencias entre copias superficiales y profundas.
Clonar un objeto significa producir un nuevo objeto con el mismo estado: una copia que puedes mutar de forma independiente al original. La solución integrada de Java es Object.clone() junto con la interfaz marcadora Cloneable, pero el diseño tiene suficientes aristas que la mayor parte del código moderno recurre a un constructor de copia o a un método de fábrica. Este capítulo muestra ambas rutas y la trampa que existe entre ellas.
La ruta integrada: Object.clone()
Object.clone() es protected y realiza una copia superficial campo a campo de la instancia. Para usarlo debes:
- Hacer que tu clase implemente
Cloneable— una interfaz marcadora sin métodos. Sin ella,clone()lanzaCloneNotSupportedException. - Sobrescribir
clone()para hacerlopublicy (generalmente) estrechar el tipo de retorno a la clase real.
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 real. El try/catch es burocracia: la excepción comprobada está declarada en Object.clone, pero nuestra clase implementa Cloneable, por lo que la excepción es inalcanzable.
Copia superficial: qué hace realmente
Una copia superficial duplica los campos inmediatos. Las referencias dentro del objeto se copian como referencias, no como nuevos objetos — así que el original y el clon comparten aquello a lo que apuntan esas referencias:
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 arrayPara primitivos y valores inmutables (String, Integer, LocalDate), la copia superficial está bien. Para subobjetos mutables, casi siempre es incorrecta: mutar el clon repercute en el original.
Copia profunda: la solución
Para obtener una copia verdaderamente independiente, sobrescribe clone() para copiar recursivamente los campos 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);
}
}Los arrays implementan Cloneable de forma nativa y se copian con .clone(). Las colecciones no: para un campo List<String> escribirías copy.items = new ArrayList<>(items); (ver ArrayList). Para un grafo de tus propios tipos mutables, cada tipo del grafo necesita participar.
Por qué Cloneable tiene mala reputación
Algunos inconvenientes hacen que clone() sea incómodo:
- Es una interfaz marcadora sin método
clone()— el propioCloneableno expone nada; el contrato reside enObject.clone(). - Omite los constructores — los campos del nuevo objeto los rellena la JVM, por lo que los invariantes que aplicas en tu constructor no se vuelven a comprobar.
- Las subclases heredan la obligación: si
Parentsobrescribeclone(), cada subclase debe mantener sincronizada la lógica de copia profunda o heredar silenciosamente una versión superficial rota. - La excepción comprobada, el cast, la llamada a
super.clone()— cada sobrescritura repite el mismo ruido.
La alternativa moderna: constructor de copia
Un constructor de copia es simplemente un constructor que recibe una instancia de la misma clase y copia sus campos:
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);Se ejecuta a través del constructor normal, por lo que los invariantes se comprueban. Es Java puro — sin interfaz marcadora, sin CloneNotSupportedException, sin cast. Las subclases simplemente escriben su propio constructor de copia que llama a super(other). La recomendación de Effective Java es preferir constructores de copia (o fábricas estáticas copyOf) antes que clone.
Las clases similares a colecciones ya siguen este patrón: new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).
¿Qué enfoque debo usar?
| Situación | Enfoque recomendado |
|---|---|
| Clase nueva y simple que controlas | Constructor de copia o fábrica estática copyOf |
| Clase con solo campos primitivos / inmutables | Cualquiera — incluso un clone() superficial es seguro |
| Clase con campos mutables (listas, arrays, objetos anidados) | Constructor de copia con copias profundas explícitas |
API existente que ya requiere Cloneable | Sobrescribir clone() y copiar profundamente los campos mutables |
| Tipo de valor que puedes rediseñar | Hazlo inmutable — entonces no se necesita ninguna copia |
La versión corta: prefiere un constructor de copia para código nuevo y recurre a clone() solo cuando un contrato existente te obliga.
Records y tipos inmutables
Los Records son inmutables, por lo que no necesitan clonación en absoluto — comparte la misma referencia en todas partes. Si necesitas una copia modificada, escribe pequeños métodos with...:
record Point(int x, int y) {
Point withX(int newX) { return new Point(newX, y); }
}Este estilo — "construir una nueva instancia con un campo cambiado" — suele ser más claro que clonar seguido de mutación.
Un ejemplo completo
Qué viene a continuación
La mayor parte de los problemas relacionados con la clonación desaparece si la clase es inmutable desde el principio — nada que copiar de forma defensiva, sin sorpresas por aliasing, segura para compartir entre hilos. El siguiente capítulo explica lo que se necesita para diseñar una clase de esa manera. Continúa con clases inmutables en Java.