Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Implementing the clone() Method - Part 1

Implementing the clone() Method - Part 1
Das Kopieren von Objekten in Java
Teil 1: Was ist clone()?  Wofür braucht man es? Warum sollte man es implementieren?

JavaSPEKTRUM, September 2002
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

Mit diesem Artikel wollen wir die Serie über die Infrastruktur  von Objekten in Java fortsetzen. Nachdem wir uns in den vergangenen Artikel ausführlich mit der Thematik "Objekt-Vergleich" befasst haben, wollen wir uns nun einem anderen Grundlagenthema zuwenden - dem Kopieren von Objekten. Wann und warum braucht man überhaupt Kopien von Objekten? Wie erzeugt man eine Kopie?  Welche Infrastruktur muss für das Kopieren zur Verfügung gestellt werden? In diesem Kontext spielen das Cloneable-Interface und die clone()-Methode eine Rolle.   Wie spielen sie zusammen?  In welcher Beziehung stehen Object.clone(), das Cloneable-Interface und die clone()-Methode der eigenen Klasse? Braucht man clone() überhaupt oder gibt es Alternativen?  Wie kopiert man Objekte mit und ohne clone()?  Was sind die Vor-und Nachteile der verschiedenen Techniken?  Diese Fragen diskutieren wir in der vorliegenden Ausgabe unserer Kolumne. In der nächsten Ausgabe werden wir dann die Implementierung von clone()diskutieren
 

Wofür braucht man clone() ?

In Java unterscheidet man zwischen Variablen vom primitivem Typ (wie char, short, int, double, etc.) und Referenzvariablen (von einem class oder interface-Typ).  Variablen von primitivem Typ enthalten einen Wert des entsprechenden Typ und sowohl beim Zuweisen und Vergleichen als auch bei der Übergabe an und Rückgabe von Methoden wird immer der enthaltene Wert übergeben bzw. verglichen.  Das ist bei Referenzvariablen anders.  Das Objekt, auf das eine Referenzvariable verweist, wird per Referenz verwaltet und herumgereicht.  Beim Zuweisen und Vergleichen per Zuweisungs- und Vergleichsoperator werden lediglich die Adressen der referenzierten Objekte zugewiesen bzw. verglichen; die referenzierten Objekte spielen gar keine Rolle. Ebenso wird bei der Übergabe an oder Rückgabe von Methoden nur die Adresse von Objekten übergeben, nicht jedoch das referenzierte Objekt selbst.

Die Referenzsemantik in Java spart Overhead, den das Kopieren der Objekte verursachen würde, führt aber andererseits zu manchmal unerwünschten Beziehungsverflechtungen.  Bisweilen ist es in Java schwer, den Überblick darüber zu behalten, wer wann zu welchem Zweck eine Referenz auf ein bestimmtes Objekt hält und was der Betreffende mit der Referenz anstellt.  Da es in der Sprache auch kein Konzept und Sprachmittel zum Schutz der referenzierten Objekte vor Modifikationen gibt, kann im Prinzip jeder, der eine Referenz auf ein Objekt hält, das referenzierte Objekt ändern.  Das kann zu überraschenden Effekten führen.

Sehen wir uns ein typisches Beispiel für die Schwierigkeiten mit der Referenzsemantik an:

class ColoredPoint {
  private Point p;
  private int color;
  public ColoredPoint (Point newP, int newColor)
  { p = newP; color = newColor; }                       // (1)
}
...
Point[] createLine(int len, int m, int c) {
  Point nextPoint = new Point(0,c);                     // (2)
  Point [] line = new Point[len];
  for (int i=0; i<len; i++) {
    line[i] = new ColoredPoint(nextPoint , 0xFF00FF);   // (3)
    nextPoint.x += 1;
    nextPoint.y += m; }
  return line;
}

Wir haben eine Klasse ColoredPoint, die einen Punkt mit zwei Koordinaten und eine Farbe enthält. Der Konstruktor der Klasse ColoredPoint bekommt Initialwerte für diese beiden Daten und merkt sie sich in entsprechenden privaten Feldern. Die Methode createLine() erzeugt ein Array von Points, das eine Linie beschreibt.  Wie sieht die Linie aus, die von createLine() erzeugt wird? Nicht ganz so, wie sich der Autor das gedacht hat.  Wo ist das Problem?

Das Problem liegt in der Referenz-Semantik der Variablen in Java. Die Methode createLine() berechnet für jeden Punkt in dem Array, das die Linie beschreiben soll, die jeweiligen Koordinaten. Diese Koordinaten werden in dem Point-Objekt nextPoint abgelegt (siehe Codezeile (2)). Dieses Point-Objekt wird benutzt, um jeweils einen neuen ColoredPoint zu erzeugen, dessen Referenz schließlich im Array abgelegt wird (siehe Codezeile (3)).

Das Missverständnis besteht darin, dass createLine() offensichtlich davon ausgeht, dass der Konstruktor von ColoredPoint sich den Point, der als Konstruktor-Argument übergeben wird, merkt, indem er sich eine Kopie davon anlegt.  Tatsächlich merkt sich der ColoredPoint-Konstruktor aber nur die Adresse des übergebenen Point-Objekts; die Zuweisung p = newP; (siehe Code-Zeile (1)) ist eine Zuweisung von Referenzvariablen und das ist in Java lediglich die Zuweisung der Objekt-Adresse, nicht des Objekt-Inhalts.  Die Linie wird also aus len-vielen Punkten bestehen, die alle die zuletzt berechneten Koordinaten  enthalten, weil sie alle auf das eine Point-Objekt nextPoint verweisen, das am Anfang der Methode createLine() erzeugt wurde.
 

Object Sharing

Das Beispiel demonstriert eine in Java typische Situation, die häufig dann auftritt, wenn Argumente an Konstruktoren übergeben werden, die der Konstruktor sich dann in Feldern der Klasse merken will.  In solchen Fällen will die Klasse oft keine Referenz auf das übergebene Objekt halten, sondern will ihre eigene Kopie davon haben. Die Kopie hat den Vorteil, dass sie nicht mit anderen Objekten geteilt werden muss. In obigem Beispiel ist genau das Gegenteil eingetreten: mehrere ColoredPoint-Konstruktoren haben sich Referenzen auf ein einziges Point-Objekt gemerkt und damit dieses Point-Objekt zum Gemeinschaftsgut gemacht. Eine solche Situation bezeichnet mal als Object Sharing und sie kann zu Problemen führen, wie in obigem Beispiel: das Object Sharing hat sich später in der createLine()-Methode negativ bemerkbar gemacht, weil das gemeinsam verwendete Point-Objekt verändert wurde.  In unserem Beispiel wäre es sicher besser gewesen, wenn der Konstruktor eine Kopie angelegt hätte und sich eine Referenz auf seine eigene Kopie des Point-Objekts gemerkt hätte. Wie man sieht, führt das Object-Sharing leicht zu Problemen und kann durch das Anlegen von Kopien vermieden werden.

Unerwünschtes Object-Sharing tritt nicht nur im Zusammenhang mit Konstruktoren auf.  Eine ähnliche Situation liegt beispielsweise vor, wenn Objekte von Methoden zurück gegeben werden.  Wenn etwa die Methode einer Klasse eine Referenz auf ein Feld der Klasse zurückliefert, dann bekommen alle Empfänger des Returnwerts eine Referenz auf ein gemeinsam verwendetes Objekt.  Auch das ist oft unerwünscht und der Empfänger will eigentlich seine eigene Kopie des zurück gelieferten Objekts haben. Um das zu erreichen, könnte die betreffende Methode jedes Mal eine Kopie anlegen und eine Referenz auf die jeweilige Kopie zurück geben.

Woher weiß man eigentlich, ob bei der Übergabe von Referenzen an und von Methoden die Gefahr eines unerwünschten Object-Sharings besteht? Woran kann man erkennen, ob eine Referenz, die von einer Methode zurückgegeben wird, auf das Original verweist oder auf eine Kopie?  Oder, betrachten wir unser Beispiel: woran hätte der Autor der createLine()-Methode erkennen können, wie der ColoredPoint-Konstruktor mit der übergebenen Point-Referenz umgeht? Ansehen kann man das einer Methode in Java nicht. Solche Details müssen in der JavaDoc-Beschreibung der Methode dokumentiert sein.  Deshalb sollten generell alle Methoden, die Referenzen bekommen oder zurückgeben, in der JavaDoc klare Aussagen über die Benutzung der Referenz machen.  Bei der Rückgabe von Referenzen muss klar sein, ob die gelieferte Referenz aufs Original-Objekt verweist und zu Objekt-Sharing führt, oder ob die Methode bereits von sich aus eine Kopie angelegt hat und eine Referenz auf diese Kopie zurückliefert.  Bei der Übergabe von Referenzen an eine Methode muss ebenfalls geklärt sein, ob die Methode intern eine Kopie des referenzierten Objekts anlegt und verwendet, oder ob die Methode mit dem referenzierten Original-Objekt arbeitet. Im letzteren Fall muss ggf. der Aufrufer vor dem Aufruf der Methode bereits eine Kopie anlegen, wenn ein Objekt-Sharing verhindert werden soll. Der Aufrufer kann zur Sicherheit immer eine Kopie anlegen, ganz egal was die gerufene Methode macht, aber das ist natürlich nicht die effizienteste Lösung, weil unter Umständen unnötig oft kopiert wird. In jedem Fall muss die Arbeitsteilung zwischen Aufrufer und Methode geklärt und in der JavaDoc dokumentiert sein. Ohne klare Beschreibung in der JavaDoc kann kein Benutzer einer Methode wissen, ob er zur Vermeidung von Objekt-Sharing vor oder nach dem Aufruf der Methode Kopien angelegen muss oder nicht.

Im Zusammenhang mit der Übergabe von Referenzen an und von Methoden kommt es nicht automatisch immer zu Object-Sharing-Situationen.  Solche Situationen treten nur auf, wenn beide (Aufrufer und gerufene Methode bzw. Klasse) das referenzierte Objekt nach dem Aufruf noch weiter verwenden wollen, wie etwa in unserem Beispiel mit createLine(): wenn createLine() darauf verzichtet hätte, das Point-Objekt, das an den ColoredPoint-Konstruktor übergeben wurde, weiter zu verwenden, dann wäre überhaupt kein problematisches Object Sharing entstanden.  Analog bei der Rückgabe von Referenzen:  wenn eine Methode eine Referenz auf ein Objekt zurück gibt, dass sie gerade eben mit new angelegt hat, dann kann auch nichts passieren.  Das Problem tritt nur auf, wenn die Methode eine Referenz zurück gibt, die auch später noch der Methode (oder anderen Methoden der Klasse) zugänglich ist, etwa weil die Referenz auf das zurück gegebene Objekt in einem Feld der Klasse abgelegt ist. Dann hat sowohl der Aufrufer über die zurückgegebene Referenz Zugriff auf das Objekt als auch die Klasse mit all ihren Methoden.  Wenn aber die zurückgegebene Referenz nirgendwo hinterlegt wurde, dann hat nur der Aufrufer Zugriff aufs Objekt und ein problematisches Object-Sharing kommt überhaupt nicht zustande.

Das Anlegen von Kopien ist im übrigen nicht die einzige Antwort auf unerwünschtes Object-Sharing.  Die oben geschilderten Probleme lassen sich unter Umständen auch ohne Kopien lösen, zum Beispiel mit Immutability-Adaptoren. Wenn das gemeinsam verwendete Objekt nämlich unveränderlich (immutable) ist, dann stört das Object Sharing nicht, und dann ist auch nicht nötig, Kopien anzulegen.  Immutability wollen wir aber in dieser Ausgabe der Kolumne nicht besprechen.  Wir wollen uns statt dessen ansehen, wie man Kopien von Objekten erzeugt, wenn man solche Kopien braucht.
 

Das Kopieren von Objekten

Für das Erzeugen von Kopien von Objekten gibt es in Java mehrere Möglichkeiten. Eine Klasse, die es ermöglichen will, dass Kopien von ihren Objekten erzeugt werden, kann eine clone()-Methode implementieren und/oder einen sogenannten Copy-Konstruktor zur Verfügung stellen. Es gibt auch noch andere Beispiele für Kopierfunktionalität, die aber ebenfalls auf Konstruktoren beruhen.

Klonen per clone()-Methode

Wenn eine Klasse eine clone()-Methode hat, dann können Kopie mit Hilfe dieser Methode erzeugt werden. clone() erzeugt ein neues Objekt vom gleichen Typ mit gleichem Inhalt und gibt eine Referenz auf das neue Objekt als Ergebnis zurück. Klassen, die eine clone()-Methode implementieren, müssen zusätzlich das Cloneable-Interface implementieren.  Das Cloneable-Interface ist ein reines Marker-Interface, d.h. es ist leer, und definiert nicht etwa die clone()-Methode, wie man erwarten könnte.  Es wird lediglich verwendet, um klonbare (cloneable) Klassen von nicht-klonbaren Klassen zu unterscheiden. Wofür das gebraucht wird, sehen wir uns später noch im Detail an.  Schauen wir erst einmal ein Beispiel für eine cloneable Klasse an. Die JDK-Klasse java.util.Date ist ein Beispiel:

public class Date implements Cloneable {
   ...
   public Object clone() { ...  }
   ...
}
 

Kopieren per Copy-Konstruktor

Wenn eine Klasse einen Copy-Konstruktor hat, dann kann man diesen Konstruktor verwenden, um Kopien zu erzeugen.  Das ist eine Alternative zur clone()-Methode.  Der Begriff "Copy-Konstruktor" stammt aus C++. Man bezeichnet damit einen Konstruktor, der ein Objekt vom eigenen Typ als Argument akzeptiert und ein neues Objekt vom gleichen Typ mit gleichem Inhalt - nämlich die Kopie - erzeugt. Die JDK-Klasse java.lang.String ist ein Beispiel für eine solche Klasse:

public final class String {
  ...
  public String(String original) { ... }
  ...
}
 

Andere Formen des Kopierens

Daneben gibt es Klassen, die werden copy-konstruierbar noch cloneable sind. Die JKD-Klasse java.lang.StringBuffer ist ein solches Beispiel:

public final class StringBuffer {
   public StringBuffer(String str) { ...  }
   public String toString() { ...  }
}

Man kann eine Kopie eines StringBuffer  erzeugen, indem man das Original in einen String konvertiert und aus diesem String einen neuen StringBuffer konstruiert:

StringBuffer copy = new StringBuffer(original.toString());

Auf die Vor- und Nachteile der verschiedenen Techniken gehen wir nächsten Artikel genauer ein.  Es wird sich herausstellen, dass clone() die für das Kopieren zu empfehlende Technik ist. Hier wollen wir uns zunächst ansehen, was von einer Implementierung der clone()-Methode genau erwartet wird.
 

Der clone()-Contract

Die Anforderungen an die clone()-Methode einer Klasse sind im sogenannte clone()-Contract beschrieben, den man in der JavaDoc unter Object.clone() findet.  Hier ist der Original-Wortlaut:
 
Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
The general intent is that, for any object x, the expression:
         x.clone() != x
will be true, and that the expression:
         x.clone().getClass() == x.getClass()
will be true, but these are not absolute requirements. While it is typically the case that:
         x.clone().equals(x)
will be true, this is not an absolute requirement.
Copying an object will typically entail creating a new instance of its class, but it also may require copying of internal data structures as well. No constructors are called.


Das bedeutet das Folgende:

  • Klon und Original sind verschiedene Objekte, d.h. sie sind an verschiedenen Stellen im Speicher angelegt.
  • Klon und Original sind vom selben Typ.
  • Klon und Original sollten gleich sein im Sinne von equals(), d.h. sie sollten den gleichen Inhalt haben.

Die Methode Object.clone()

Wenn man eine Klasse cloneable machen will, dann muss die Klasse das Cloneable-Interface implementieren und eine clone()-Methode, typischerweise mit der Signatur public Object clone(), definieren.  Im einfachsten Fall implementiert man die clone()-Methode, indem man super.clone() aufruft.

class MyClass implements Cloneable {
  ...
  public Object clone() {
    try { return super.clone(); } catch (CloneNotSupportedException e) {}
  }
  ...
}

In diesem einfachen Fall hat man einfach nur die geerbte Methode Object.clone() als public-Methode zugänglich gemacht. Die Superklasse Object hat nämlich eine clone()-Methode, aber die ist protected und steht damit im public Interface ihrer Subklassen nicht automatisch zur Verfügung. Deshalb sind Java-Klassen zunächst einmal nicht cloneable;  man muss die clone()-Methode erst einmal zugänglich machen.

Dazu genügt es nicht, eine public clone()-Methode zur Verfügung zu stellen, sondern die Klasse muss zusätzlich das Cloneable-Interface implementieren, sonst gibt es eine CloneNotSupportedException. Das Implementieren des Cloneable-Interface ist nötig, weil Object.clone() prüft, ob das this-Object von einem Typ ist, der das Cloneable-Interface implementiert.  Falls man clone() auf einem Objekt aufruft, das nicht cloneable ist, dann wirft Object.clone() eine CloneNotSupportedException. Das Zugänglich-Machen der clone()-Methode reicht also noch nicht; die Klasse muss außerdem immer auch das Cloneable-Interface implementieren.

Bei dieser Prüfung wird deutlich, dass das Cloneable-Interface als Marker-Interface dient: die Methode Object.clone() verwendet es zur Unterscheidung zwischen klonbaren und nicht-klonbaren Objekten.

  • Die nicht-klonbaren Objekte dürfen nicht geklont werden; deshalb wird eine CloneNotSupportedException geworfen.
  • Für die klonbaren Objekte erzeugt Object.clone() einen Klon. Dazu alloziert Object.clone() den benötigten Speicher und kopiert alle Felder des Objekts "as if by assignment", wie es in der Spezifikation heißt.  Das bedeutet, dass Object.clone()alle Felder von this an die korrespondierenden Felder des neu erzeugten Objekts zuweist, was bei Referenzen heißt, dass nur die Referenz, nicht aber das referenzierte Objekt kopiert wird.  Solche Kopien bezeichnet man als "flache Kopie" (shallow copy), im Gegensatz zur "tiefen Kopie" (deep copy), bei der alle Referenzen rekursiv verfolgt und auch die referenzierten Objekte kopiert werden.
Arrays werden im übrigen implizit als cloneable angesehen und haben eine clone()-Methode, die eine "shallow copy" des Arrays anlegt: es werden alle Felder des Arrays kopiert; wenn die Felder Referenzen sind, werden nur die Referenzen, nicht aber die referenzierten Objekte kopiert.

Object.clone() ist als native Methode implementiert, d.h. sie ist nicht in Java, sondern in einer anderen Programmiersprache implementiert. Im Prinzip kann man sich die Implementierung der Methode Object.clone() so vorstellen, dass sie erst prüft, ob das this-Objekt cloneable ist. Wenn ja, dann wird Speicher in ausreichender Menge besorgt und der Inhalt von this wird bitweise kopiert.  Das ergibt dann genau den Effekt einer "shallow copy".
 

Shallow Copy vs. Deep Copy

In unserem Beispiel einer ersten einfachen Implementierung von MyClass.clone() (siehe oben) haben wir eine clone()-Methode implementiert, die eine flache Kopie des Originals erzeugt.  Nun ist die Frage: ist diese Implementierung korrekt? Oder anders gesagt, wann sind flache Kopien ausreichend bzw. unzureichend? Sehen wir uns das einmal am Beispiel der clone()-Methode von Arrays an, die ja ebenfalls eine flache Kopie des Arrays erzeugt.

Point[] pa1 = { new Point(1,1), new Point(2,2) };
Point[] pa2 = null;

try {
 pa2 = (Point[]) pa1.clone();
} catch (CloneNotSupportedException e) { ... }

Hier wird ein Point-Array geklont per Aufruf der clone()-Methode für Arrays. Danach sieht die Situation wie folgt aus:

Was passiert, wenn man eines der beiden Point-Arrays manipuliert?  Hier ist ein Beispiel mit ein paar Modifikationen des Klons pa2:

pa2[0] = new Point(-2,-2);
pa2[0].x =  2;
pa2[1].y = -2;

Auf den ersten Blick würde man annehmen, dass nur der Klon pa2 sich ändert, weil alle Zuweisungen im gezeigten Code sich auf pa2 beziehen.   Aber so einfach ist das nicht. Da der Klon eine flache Kopie des Originals ist, wirken sich einige der Modifikationen auch auf das Original pa1 aus:

Das ist nicht ganz das, was man sich unter einem Klon vorstellt.  Die Idee des Klonens oder Kopierens ist, dass Original und Kopie voneinander unabhängig sind, d.h. Veränderungen des einen Objekts sollen keine Auswirkungen auf das andere Objekt haben. Das ist hier ganz offensichtlich nicht erreicht worden; das geklonte Array erfüllt die Unabhängigkeitsanforderung nicht . Um eine Unabhängigkeit von Original und Klon zu erreichen, müssten wir hier eine tiefe Kopie machen.  Das könnte man wie folgt implementieren:

Point[] pa1 = { new Point(1,1), new Point(2,2) };
Point[] pa2 = null;

try {
  pa2 = pa1.clone();
  pa2[0] = (Point) pa2[0].clone();
  pa2[1] = (Point) pa2[1].clone();
} catch (CloneNotSupportedException e){ ... }

Hier wird nicht nur das Array, sondern es werden auch alle Array-Elemente kopiert.  Jetzt haben Original und Klon wirklich nichts mehr miteinander zu tun und Veränderungen des einen betreffen den anderen nicht.


 

Wann ist eine Kopie "tief genug"?


Wie das Beispiel zeigt, erreicht man mit einer tiefen Kopie das angestrebte Ziel, nämlich dass Original und Klon voneinander unabhängig sind.  Der Aufwand für die tiefe Kopie ist aber nicht in allen Fällen erforderlich.  Man unterscheidet 3 Fälle:

  • Arrays von primitivem Typ
  • Arrays von Referenzen auf unveränderliche Objekte
  • Arrays von Referenzen auf veränderliche Objekte
Arrays von primitivem Typ. Nehmen wir als Bespiel ein Array von int-Werten, das wir klonen wollen. Die flache Kopie, die von clone() für Arrays erzeugt wird, ist bereits tief genug. Das liegt daran, dass Variablen von primitivem Typ ihre Werte enthalten und nicht auf sie verweisen. Deshalb ist die bitweise Kopie des Array-Elements, die von clone() erzeugt wird, tatsächlich eine Kopie des int-Werts selbst und es ist über den Aufruf von clone() für das Arrray hinaus nichts weiter zu tun, um einen echten Klon eines int-Arrays zu erzeugen.
int[] ia1 = { 1, 2 };
int[] ia2 = null;

try {
   ia2 = (int[]) ia1.clone(); 

} catch (CloneNotSupportedException e) { ... }
Abbildung 1: Klonen eines Arrays von primitivem Typ

Arrays von Referenzen auf unveränderliche Objekte. Für ein Array von Referenzen auf unveränderliche Objekte ist die flache Kopie, die von clone() für Arrays erzeugt wird, bereits tief genug. Das Ergebnis der flachen Kopie sind zwei Arrays, die die gleichen Adressen enthalten und damit auf dieselben Objekte verweisen.  Da die referenzierten Objekte aber nicht verändert werden können, ist das Object-Sharing unproblematisch.   Betrachten wir als Beispiel ein Array von Strings:

String[] sa1 = { "one", "two" };
String[] sa2 = null;

try {
 sa2 = (String[]) sa1.clone(); 
} catch (CloneNotSupportedException e) { ... }

Abbildung 2: Klonen eines Arrays von Referenzen auf unveränderliche Objekte

Warum sind Original und Kopie in diesem Beispiel voneinander unabhängig, obwohl sie alle Array-Elemente gemeinsam referenzieren?  Sehen wir uns an, welche Modifikation überhaupt auftreten können. Veränderungen des Originals und der Kopie betreffen jeweils nur die Arrays selbst, aber niemals die gemeinsam verwendeten String-Objekte. Die beiden gemeinsam referenzierten Strings "one" und "two" können nicht modifiziert werden, weil die Klasse String keine modifizierenden Methoden zur Verfügung stellt.  Ein Aufruf wie sa1[1].concat(" steps") zum Beispiel sieht zwar so aus, als verändere er den String "two", aber in Wirklichkeit erzeugt dieser Aufruf einen neuen String mit Inhalt "two steps". Dieser neue String kann dann den alten ersetzen, z.B. durch sa1[1] = sa1[1].concat(" steps"), aber davon ist das andere Array sa2 nicht betroffen.

Arrays von Referenzen auf veränderliche Objekte. Das ist der Fall, den wir am Beispiel des Point-Arrays bereits ausführlich diskutiert haben. Hier reicht die flache Kopie nicht und es müssen neben dem Array auch alle referenzierten veränderlichen Array-Elemente kopiert werden, damit Original und Klon voneinander unabhängig sind.

Point[] pa1 = { new Point(1,1), new Point(2,2) };
Point[] pa2 = null;

try {
  pa2 = pa1.clone();
  pa2[0] = (Point) pa2[0].clone(); 
  pa2[1] = (Point) pa2[1].clone();
} catch (CloneNotSupportedException e){ ... }

Abbildung 3: Klonen eines Arrays von Referenzen auf veränderliche Objekte










Was wir hier am Beispiel von Arrays beschrieben haben, gilt ganz analog auch für Klassen.

Arrays haben Elemente , die entweder von primitivem Typ sind oder aber Referenzen auf veränderliche oder unveränderliche Objekte.  Die clone()-Methode für Arrays kopiert sämtliche Elemente bitweise. Je nach Art der Elemente genügt das oder es muss eine tiefe Kopie gemacht werden, wie oben beschrieben.

Objekte , d.h. Instanzen von Klassen, haben Felder , die entweder von primitivem Typ sind oder aber Referenzen auf veränderliche oder unveränderliche Objekte.  Wenn man die clone()-Methode für eine solche Klasse implementieren will, dann wird man alle nicht-statischen Felder kopieren, so wie das Array-clone() sämtliche Array-Elemente kopiert.

Ganz analog zum Array stellt sich auch hier die Frage: genügt eine bitweise Kopie der Felder oder müssen Referenzen verfolgt werden und tiefe Kopien angelegt werden? Die Antwort ist dieselbe wie für Arrays:  wenn die Felder von primitiven Typ sind oder Referenzen auf unveränderliche Objekte, dann genügt normalerweise die bitweise Kopie (die übrigens von Object.clone() bereits erzeugt wird).  Wenn die Felder Referenzen auf veränderliche Objekte sind, dann muss eine tiefe Kopie angelegt werden.

Allgemein kann man die Regel für die Tiefe der beim Klonen zu erzeugenden Kopie wie folgt formulieren: das Original-Objekt (oder -Array) und sein Klon müssen so unabhängig voneinander sein, dass keine Operation auf dem Original den Klon betrifft und umgekehrt. Alle Implementierungen von clone() sollten dieser Anforderung genügen. In der Praxis findet man manchmal clone()-Methoden, die keine ausreichend tiefe Kopie liefern; clone() für Arrays ist ein Beispiel, wie wir oben gesehen haben.  Solche Implementierungen sollte man bei eigenen Klassen vermeiden.  Es kann zwar vorkommen, dass man keine hinreichend tiefe Kopie erzeugen kann (wir werden im nächsten Artikel sehen warum), aber in solchen Fällen sollte man dann lieber gar kein clone() als ein inkorrektes clone() zur Verfügung stellen.

Und noch ein Hinweis:  Wenn eine Klasse keine clone()-Methode definiert, dann sollte sie auch nicht das Cloneable-Interface implementieren.  Dann klingt zwar fast wie ein Witz, kann aber vorkommen, weil das Cloneable -Interface leer ist.  Man kann in der Tat (absichtlich oder versehentlich) eine Klasse definieren, die das Cloneable-Interface implementiert, aber keine clone()-Methode hat.  Das ist zwar von der  Logik her widersinnig, aber syntaktisch völlig in Ordnung.  Der Compiler lässt das durchgehen, weil das Cloneable -Interface keine einzige Methode vorschreibt, auch keine clone()-Methode.

clone() und Generische Collections

Das leere Cloneable-Interface macht auch sonst noch Schwierigkeiten, beispielsweise beim Kopieren von generischen Collections.   Unter generischen Collections versteht man heterogene Container, die Elemente verschiedenen Typs enthalten.  Das einfachste Beispiel ist ein Array von Objects.  Jedes Array-Element ist eine Referenz auf ein Objekt eines beliebigen Klassen- oder Interface-Typs in Java.  Wie kann man so ein Object-Array klonen oder kopieren?

public class MyClass implements Cloneable {
  private Object oa[];

  public Object clone() {
    Object[] tmp = oa.clone();

    for (int i=0; i<oa.length; i++)
       ... clone each array element ...
    return tmp;
  }
}

Für solche Situationen gibt es die clone()-Methode.  Im Prinzip ist es so gedacht, dass man für jedes Array-Element die clone()-Methode aufruft, vorausgesetzt das Array-Element ist überhaupt cloneable.  clone() ist eine non-final Methode und so würde dann für jedes Element, egal welchen Typs es zur Laufzeit ist, die clone()-Methode dieses Typs angestoßen. Das würde dann so aussehen:

    ...
    for (int i=0; i<oa.length; i++)
      if (oa[i] instanceof Cloneable) {
         tmp[i] = ((Cloneable) oa[i]).clone();
      }
    ...

Leider beschwert sich aber der Compiler über diesen wohlgemeinten Versuch, die clone()-Methode aufzurufen - und zu recht.  Wir haben zwar ordnungsgemäß von Object nach Cloneable gecastet, um clone() nur dann aufzurufen, wenn das Objekt cloneable ist, und um die CloneNotSupportedException zu vermeiden.  Aber da das Cloneable-Interface leer ist, gibt uns der Cast keinen Zugriff auf die clone()-Methode.  So geht's also nicht; per Cast haben wir keine Chance clone() aufzurufen, solange wir den echten Typ des Objekts nicht kennen. Da bleibt dann nur eine Lösung: man muss sich zur Laufzeit Information darüber beschaffen, ob das Objekt von einem Typ ist, der die clone()-Methode implementiert und wenn ja, dann muss man diese clone()-Methode aufrufen.  Für solche Aufgaben gibt es Reflection in Java.
 

Aufruf von clone() über Reflection

Im Package java.lang.reflect (zum Teil auch im Package java.lang) liefert der JDK Funktionalität, mit der man zur Laufzeit Meta-Information über Java-Typen beschaffen und benutzen kann. Man kann sich mit Hilfe der Methode getClass(), die bereits in Object definiert ist, ein Objekt vom Typ Class geben lassen, welches den Typ des Objekts repräsentiert, auf dem die getClass()-Methode aufgerufen wurde.  Mit diesem Class-Objekt kann u.a. Information über Felder und Methoden der Klasse besorgt werden.  In unserem Fall interessieren wir uns für eine bestimmte Methode, nämlich die clone()-Methode, die wir aufrufen wollen.  Das sieht wie folgt aus:

...
for (int i=0; i<oa.length; i++)
    if (oa[i] instanceof Cloneable) {
      try {
        tmp[i] = oa[i].getClass()
                 .getMethod("clone", null)
                 .invoke(oa[i], null);
      } catch (Exception e) {
        throw new CloneNotSupportedException();
      }
    }
 ...

Auf die Details von Reflection wollen wir an dieser Stelle nicht weiter eingehen.  Es sei aber angemerkt, dass Methoden-Aufrufe über Reflection nicht nur umständlicher und wesentlich unleserlicher sind als normale Aufrufe, sie sind auch deutlich aufwendiger und langsamer.  Laufzeit-Unterschiede in der Größenordnung von 1:100 (je nach Virtueller Maschine und System-Kontext) sind nicht unrealistisch.  Außerdem können bei Benutzung von Reflection zur Laufzeit wesentlich mehr Fehler auftreten als beim normalen statischen Aufruf. Beispiel: wenn man sich beim Methoden-Namen vertippt hat, dann merkt das normalerweise der Compiler; beim Aufruf der Methode über Reflection wird dieser Fehler erst beim Programmablauf bemerkt und führt zu einer Exception, auf die das Programm sinnvoll reagieren muss. Die Nachteile des Methoden-Aufrufs über Reflection sind verglichen mit dem statischen Methodenaufruf gravierend. Man wird deshalb normalerweise immer den statischen Aufruf vorziehen. Beim Klonen von generischen Collections kann man die Nachteile durch die Reflection-Nutzung aber leider nicht vermeiden, weil wegen dem leeren Cloneable-Interface der statische Aufruf überhaupt nicht möglich ist.
 

Non-Cloneable Objekte in Generischen Collections

Beim Kopieren von generischen Collections hat man nicht nur Probleme mit dem leeren Cloneable-Interface, sondern der Container könnte auch Referenzen auf Objekte enthalten, die tatsächlich gar nicht cloneable sind.  Da hilft dann auch Reflection nichts mehr und man muss zu anderen Kopier-Techniken greifen.  Dazu muss man aber sämtliche non-cloneable Typen per Fallunterscheidung identifizieren und die für den jeweiligen Typ passende Kopiertechnik kennen.  Hier sind ein paar Beispiele:

for (int i=0; i<oa.length; i++)
    if (oa[i] instanceof Cloneable) {
      ... call clone via reflection ...
    }
    else if (oa[i] instanceof StringBuffer)
      tmp[i] = new StringBuffer (oa[i].toString());

    else if (oa[i] instanceof String)
      tmp[i] = oa[i];
    ...
    // all other types
    ...
    else throw new CloneNotSupportedException();

Diese Fallunterscheidung ist natürlich ein Albtraum was Erweiterbarkeit und Pflege der Software angeht.  Deshalb ist anzuraten, dass alle Klassen, die Value-Typen (siehe unten) repräsentieren, clone() unterstützen sollten, auch wenn sie alternative Kopiertechniken, z.B. per Copy-Konstruktor, anbieten.  Diese Erkenntnis hat sich in der Java-Community erst langsam durchgesetzt, wie man an der Geschichte des JDK sehen kann.  In frühen Versionen des JDK (1.0 und 1.1) waren viele Klasse nicht cloneable, die man später cloneable gemacht hat; ein Beispiel ist die Klasse java.util.Date.  Offenbar hat es sich als reales Problem herausgestellt, wenn jede Klasse ihre eigene Technik für das Erzeugen von Kopien entwickelt.

Die Unterscheidung zwischen Value- und Entity-Typen hatten wir bereits in einem der vorangegangenen Artikel erläutert (siehe / KRE /), als wir überlegt haben, welche Klassen equals() implementieren müssen (Value-Typen) und für welche Typen das nicht nötig ist (Entity-Typen).  equals(), hashCode(), compareTo() und auch clone() sind Methoden, die nur für Value-Typen von Bedeutung sind, weil sich die Semantik dieser Methoden um den Inhalt des Objekts dreht.  Bei Entity-Typen ist der Inhalt des Objekt nicht von so herausragender Bedeutung, so dass Entity-Typen meistens keine dieser Methoden implementieren (oder nur eine sehr simple auf der Adresse des Objekts basierende Implementierung haben).

Wenn man sich den clone()-Contract ansieht, kann man auch sehen, warum Entity-Typen keine clone()-Methode haben.  Der clone()-Contract verlangt, dass x.clone() != x ist und dass x.clone().equals(x) ist, d.h. Klon und Original müssen verschiedene Objekte mit gleichem Inhalt sein.  Nun ist es aber für Entity-Typen so, dass der ==-Operator und die equals()-Methode dieselbe Semantik haben: beide prüfen auf Identität der zu vergleichenden Objekte.  Das liegt daran, dass für Entity-Typen die equals()-Methode nicht implementiert wird; dann gibt es nur die von Object geerbte equals()-Methode und die vergleicht die Adressen der Objekte, genau wie das der ==-Operator macht.  Unter diesen Umständen kann ein Entity-Typ keine clone()-Methode haben, die dem clone()-Contract genügt: wenn Klon und Original verschiedene Objekte sind (d..h. x.clone() != x), dann liefert x.clone().equals(x) das Ergebnis false und der equals()-Contract wäre verletzt.
 

Das Klonen von unveränderlichen Objekten

Grundsätzlich sollte man clone() implementieren für alle Klassen mit Value-Semantik, es sei denn,  es gibt gute Gründe, es nicht zu tun.  Ein guter Grund liegt vor, wenn die Klasse unveränderliche (immutable) Objekte beschreibt, also keine modifizierenden Methoden anbietet.  Objekte eines solchen Typs können niemals verändert werden. Man kann argumentieren, dass unveränderliche Objekte niemals kopiert werden müssen, weil man problemlos Referenzen darauf halten kann und das resultierende Object-Sharing bei unveränderlichen Objekten einfach kein Problem ist.

Dieser Logik folgend müssten dann alle veränderlichen Value-Typen cloneable sein und alle unveränderlichen non-cloneable. Leider ist das in der Praxis nicht so. Man kann sich keineswegs darauf verlassen, dass eine non-cloneable Klasse genau deshalb kein clone() hat, weil man keine Kopien braucht und die Objekte problemlos gemeinsam referenzieren kann.  Die Klasse java.lang.String beispielsweise folgt dieser Regel; sie ist unveränderlich und non-cloneable. Aber bei der Klasse java.lang.StringBuffer stimmt es schon nicht mehr; sie ist non-cloneable, aber trotzdem veränderlich und keineswegs problemlos beim Object-Sharing. Aus der Tatsache, dass eine Klasse nicht cloneable ist, kann man daher nicht ableiten, dass keine Kopien von Instanzen dieser Klasse gebraucht werden.  Der umgekehrte Schluss ist auch nicht möglich: aus der Tatsache, dass eine Klasse cloneable ist, kann man nicht ableiten, dass Kopien gebraucht werden.

Für eigene Klassen ist es empfehlenswert, sich eine klare Strategie zu überlegen, nämlich die 1:1-Beziehung zwischen "Für Instanzen dieser Klasse ist das Object-Sharing problematisch." und "Die Klasse ist cloneable."  Dann kommt man automatisch dazu, dass alle veränderlichen Value-Typen cloneable sind und alle unveränderlichen Value-Typen  non-cloneable sind und alle Entity-Typen ebenfalls non-cloneable sind.

Zusammenfassung und Ausblick

In diesem Artikel haben wir uns angesehen, warum das Kopieren in Java überhaupt eine Rolle spielt. Wir haben verschiedene Techniken dafür gesehen (im wesentlichen Klonen und Copy-Konstruktion) und festgestellt, dass es empfehlenswert ist, zumindest für veränderliche Value-Typen die clone()-Methode zu implementieren.  Wir haben die Anforderung an clone() (den sogenannten clone()-Contract) gesehen und uns überlegt, wie tief eine Kopie sinnvollerweise sein sollte. Und schließlich haben wir uns mit einigen Eigenarten des leeren Cloneable-Interface befasst.

Worauf man achten muss, wenn man clone()implementiert, werden wir in der nächsten Ausgabe der Kolumne untersuchen (siehe / CLON /). Dabei wird u.a. die besondere Rolle von Object.clone() deutlich werden, die wir bislang kaum erwähnt haben. Wir werden sehen, wo die Copy-Konstruktion als Alternative zum Klonen ihre Grenzen hat. Im übernächsten Artikel werden wir dann noch die CloneNotSupportedException diskutieren.

Literaturverweise

 
/KRE/ Objekt-Vergleich per equals(), Teil 1 und 2 
Klaus Kreft & Angelika Langer
Java Spektrum, Januar 2002 und März 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/01.Equals-Part1/01.Equals1.html
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/02.Equals-Part2/02.Equals2.html
/KRE2/ Secrets of equals()
Part 1: Not all implementations of equals() are equal
Part 2: How to implement a correct slice comparison in Java
Angelika Langer & Klaus Kreft
Java Solutions, April 2002 and August 2002
URL: http://www.AngelikaLanger.com/Articles/Java/SecretsOfEquals/Equals.html
URL: http://www.AngelikaLanger.com/Articles/Java/SecretsOfEquals/Equals-2.html
/CLON/ Das Kopieren von Objekten in Java (Teil 2 + 3) 
Klaus Kreft & Angelika Langer
JavaSpektrum, November 2002 + Januar 2003
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/06.Clone-Part2/06.Clone-Part2.html
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/07.Clone-Part3/07.Clone-Part3.html
/HAG/ Practical Java - Programming Language Guide, Praxis 64
Peter Haggar
Addison-Wesley, 2000
ISBN: 0201616467
/BLO/  Effective Java Programming Language Guide
Josh Bloch 
Addison-Wesley, June 2001 
ISBN: 0201310058
/DAV/  Durable Java: Hashing and Cloning
Mark Davies 
Java Report, April 2000
URL: http://www.macchiato.com/columns/Durable6.html
/JDK/  Java 2 Platform, Standard Edition v1.3.1
URL: http://java.sun.com/j2se/1.3/
/JDOC/  Java 2 Platform, Standard Edition, v 1.3.1 - API Specification
URL: http://java.sun.com/j2se/1.3/docs/api/index.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2008 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/05.Clone-Part1/05.Clone-Part1.html  last update: 26 Nov 2008