Java-Objekt-Klonen
Java-Objekte mit clone(), dem Cloneable-Interface und dem Unterschied zwischen flachen und tiefen Kopien kopieren.
Klonen eines Objekts bedeutet, ein neues Objekt mit demselben Zustand zu erzeugen — eine Kopie, die unabhängig vom Original verändert werden kann. Javas eingebaute Lösung ist Object.clone() zusammen mit dem Marker-Interface Cloneable, aber das Design hat genug Schwachstellen, dass moderner Code meist auf einen Copy-Konstruktor oder eine Factory-Methode zurückgreift. Dieses Kapitel zeigt beide Wege und die Falle, die zwischen ihnen liegt.
Der eingebaute Weg: Object.clone()
Object.clone() ist protected und erstellt eine flache Kopie des Objekts, Feld für Feld. Um es zu verwenden, muss man:
- Die Klasse das Interface
Cloneableimplementieren lassen — ein Marker-Interface ohne Methoden. Ohne es wirftclone()eineCloneNotSupportedException. clone()überschreiben, um espubliczu machen und (üblicherweise) den Rückgabetyp auf die eigentliche Klasse einzuschränken.
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() erzeugt die eigentliche Kopie. Der try/catch-Block ist Verwaltungsaufwand: Die geprüfte Ausnahme ist in Object.clone deklariert, aber da unsere Klasse Cloneable implementiert, ist die Ausnahme unerreichbar.
Flache Kopie: was sie tatsächlich tut
Eine flache Kopie dupliziert die unmittelbaren Felder. Referenzen innerhalb des Objekts werden als Referenzen kopiert, nicht als neue Objekte — Original und Klon teilen sich also, worauf diese Referenzen zeigen:
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 arrayBei primitiven und unveränderlichen Werten (String, Integer, LocalDate) ist eine flache Kopie in Ordnung. Bei veränderlichen Unterobjekten ist sie fast immer falsch — eine Änderung am Klon wirkt sich auch auf das Original aus.
Tiefe Kopie: die Lösung
Um eine wirklich unabhängige Kopie zu erhalten, überschreibt man clone() und kopiert veränderliche Felder rekursiv:
@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);
}
}Arrays implementieren Cloneable nativ und werden mit .clone() kopiert. Collections dagegen nicht — bei einem List<String>-Feld schreibt man copy.items = new ArrayList<>(items); (siehe ArrayList). Bei einem Graphen eigener veränderlicher Typen muss jeder Typ im Graphen mitmachen.
Warum Cloneable einen schlechten Ruf hat
Einige Eigenheiten machen clone() umständlich:
- Es ist ein Marker-Interface ohne
clone()-Methode —Cloneableselbst legt nichts offen, der Vertrag liegt beiObject.clone(). - Es umgeht Konstruktoren — die Felder des neuen Objekts werden von der JVM gefüllt, sodass im Konstruktor erzwungene Invarianten nicht erneut geprüft werden.
- Unterklassen erben die Verpflichtung: Wenn
Parentclone()überschreibt, muss jede Unterklasse die Tiefkopier-Logik synchron halten, oder sie erbt stillschweigend eine fehlerhafte flache Version. - Die geprüfte Ausnahme, der Cast, der
super.clone()-Aufruf — jede Überschreibung wiederholt dieselben Formalitäten.
Die moderne Alternative: Copy-Konstruktor
Ein Copy-Konstruktor ist schlicht ein Konstruktor, der eine Instanz derselben Klasse entgegennimmt und deren Felder kopiert:
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);Er durchläuft den normalen Konstruktor, sodass Invarianten geprüft werden. Er ist reines Java — kein Marker-Interface, keine CloneNotSupportedException, kein Cast. Unterklassen schreiben einfach ihren eigenen Copy-Konstruktor, der super(other) aufruft. Die Empfehlung von Effective Java lautet, Copy-Konstruktoren (oder statische copyOf-Factories) gegenüber clone zu bevorzugen.
Collections-ähnliche Klassen folgen diesem Muster bereits: new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).
Welchen Ansatz soll ich verwenden?
| Situation | Empfohlener Ansatz |
|---|---|
| Neue, einfache Klasse unter eigener Kontrolle | Copy-Konstruktor oder statische copyOf-Factory |
| Klasse mit nur primitiven / unveränderlichen Feldern | Beliebiger — selbst ein flaches clone() ist sicher |
| Klasse mit veränderlichen Feldern (Listen, Arrays, verschachtelte Objekte) | Copy-Konstruktor mit expliziten Tiefkopien |
Bestehende API, die Cloneable bereits erfordert | clone() überschreiben und veränderliche Felder tief kopieren |
| Werttyp, der neu gestaltet werden kann | Unveränderlich machen — dann ist gar keine Kopie nötig |
Kurz gesagt: Für neuen Code ist ein Copy-Konstruktor vorzuziehen; auf clone() greift man nur zurück, wenn ein bestehender Vertrag es erzwingt.
Records und unveränderliche Typen
Records sind unveränderlich und brauchen daher überhaupt kein Klonen — einfach dieselbe Referenz überall weitergeben. Benötigt man eine veränderte Kopie, schreibt man kleine with...-Methoden:
record Point(int x, int y) {
Point withX(int newX) { return new Point(newX, y); }
}Dieser Stil — „eine neue Instanz mit einem geänderten Feld erzeugen" — ist üblicherweise klarer als Klonen gefolgt von Mutation.
Ein ausgearbeitetes Beispiel
Was kommt als Nächstes
Der Großteil der Probleme rund ums Klonen entfällt, wenn die Klasse von Anfang an unveränderlich ist — nichts defensiv zu kopieren, keine Aliasing-Überraschungen, sicher für die gemeinsame Nutzung in Threads. Das nächste Kapitel erklärt, was es braucht, um eine Klasse so zu gestalten. Weiter zu Java unveränderliche Klassen.