Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | 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 
NEWSLETTER 
CONTACT 
Implementing the clone() Method - Part 2

Implementing the clone() Method - Part 2
Das Kopieren von Objekten in Java
Teil 2: Wie implementiert man die clone() Methode?

JavaSPEKTRUM, November 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 ).

 

Im letzten Artikel / KRE1 / dieser Serie haben wir uns angesehen, warum das Kopieren in Java überhaupt eine Rolle spielt. Wir haben verschiedene Kopier-Techniken gesehen (im wesentlichen Klonen und Copy-Konstruktion) und festgestellt, dass es empfehlenswert ist, zumindest für veränderliche Value-Typen die clone()-Methode immer zu implementieren.  Wir haben die Anforderung an clone() (den sogenannten clone()-Contract) gesehen und uns überlegt, wie tief eine Kopie sinnvollerweise sein sollte.

Worauf man achten muss, wenn man clone()implementiert, werden wir in der dieser Ausgabe der Kolumne untersuchen.
Insbesondere in Klassenhierarchien ergeben sich weitere Anforderungen und Komplikationen, die wir uns im Detail ansehen werden. Dabei werden wir die besondere Rolle untersuchen, die Object.clone() bei der Implementierung von clone() spielt. Wir werden außerdem sehen, wo die Copy-Konstruktion als Alternative zum Klonen ihre Grenzen hat. Und schließlich werden wir noch zeigen, dass final-Felder beim Klonen besondere Probleme bereiten.
 

Prinzipien der Implementierung von clone()

Versuchen wir uns an einer Implementierung von clone(). Beginnen wir mit der Signatur.

Die Signatur von clone() und die CloneNotSupportedException

Ein Klasse, die die clone()-Methode implementieren will, muss normalerweise das Cloneable-Interface implementieren. Das Cloneable-Interface ist ein leeres Marker-Interface, das dazu verwendet wird, um klonbare von nicht-klonbaren Objekten zu unterscheiden.  Da das Cloneable-Interface leer ist, gibt es keine zwingende Vorschrift, was die Signatur der clone()-Methode einer Klasse angeht.  Aus diesem Grunde verwenden viele Programmierer die Signatur von Object.clone(), nämlich

Object clone() throws CloneNotSupportedException;

Den Return- und den Argumenttyp betreffend gibt es keine Diskussionen. Man will die protected Version der Superklasse Object mit einer public Version überschreiben, also müssen Returntyp und Argumentenliste exakt dieselben sein wie in Object.clone().  Aber über die Exception-Liste kann man geteilter Meinung sein.

Es ist ein offensichtlicher Widerspruch, in einer Klasse eine public-Methode clone() zu implementieren, gerade mit dem Ziel, das Klonen zu unterstützen, und dann gleichzeitig zu sagen: diese Klasse unterstützt das Klonen eigentlich gar nicht und wird unter Umständen eine CloneNotSupportedException werfen. Das ist unlogisch und aus diesem Grunde deklariert man die clone()-Methode typischerweise als Methode, die keine checked Exceptions wirft (und damit insbesondere keine CloneNotSupportedException). [ 1 ]

[1] Zu den Vor- und Nachteilen von unchecked und checked Exceptions und deren Verwendung aus Design-Sicht findet man interessante Überlegungen in Buch von Barbara Liskov (siehe / LIS /).

In der Praxis findet man trotz des offensichtlichen Widerspruchs Implementierungen von clone(), die zwar deklarieren, dass sie eine CloneNotSupportedException werfen könnten, aber dann niemals eine solche werfen.  Wegen dieser verwirrenden Situation werden wir den nächsten Beitrag in dieser Kolumne ausschließlich der CloneNotSupportedException widmen. In diesem Artikel wollen wir clone() implementieren und wir werden es aus oben geschilderten Gründen ohne throws-Klausel deklarieren und auch so implementieren.
 

Die Funktionalität von clone()

Die Anforderungen an die Funktionalität von clone() haben wir uns im letzten Artikel bereits angesehen.  Im wesentlichen bestehen sie darin, dass clone() ein neues Objekt anlegen muss, das inhaltlich gleich dem Original, aber vom Original unabhängig  ist.

Dabei bedeutet "unabhängig", dass die Kopie so tief sein muss, dass jedwede Manipulation des Klons das Original nicht betrifft und umgekehrt.  Das läuft darauf hinaus, dass alle Referenzen verfolgt und die referenzierten Objekte kopiert werden müssen, es sei denn, sie sind unveränderlich.  Im Falle von unveränderlichen Feldern reicht es, nur die Referenzen, nicht aber die referenzierten Objekte zu kopieren.

Die geforderte "inhaltliche Gleichheit" bedeutet "Gleichheit im Sinne von equals()", d.h. der Vergleich von Klon und Original mit Hilfe von equals()muss true liefern.

Um die geforderte Funktionalität zu liefern, müssen folgende Aufgaben von der Implementierung einer clone()-Methode erledigt werden:

  • Die Methode muss Speicher beschaffen für den Klon.
  • Sie muss alle relevanten Felder in ausreichender Tiefe kopieren.

Speicherbeschaffung

Für die Speicherbeschaffung ist Object.clone() zuständig. Deshalb muss jede Implementierung von clone() als erstes super.clone() aufrufen, damit rekursiv am Ende Object.clone() angestoßen wird.  Object.clone() beschafft den Speicher fürs gesamte Objekt (abhängig vom Laufzeittyp) und füllt diesen Speicher mit einer bitweise Kopie des Originals.  Die Details der Speicherbeschaffung und -initialisierung sehen wir uns später noch an.

Wir haben im letzten Artikel schon festgestellt, dass diese bitweise Kopie nur in wenigsten Fällen eine ausreichend tiefe Kopie ist. Immer wenn die Klasse Felder hat, die Referenzen auf modifizierbare Objekte sind, dann müssen sich die clone()-Methoden der Subklassen anschließend noch um das Herstellen der hinreichend tiefen Kopie kümmern.
 

Kopieren der relevanten Felder

Jede clone()-Methode muss also super.clone() aufrufen und sich danach um die Felder der eigenen Klasse kümmern. Für die Felder von primitiven Typ ist nichts zu tun, weil Object.clone() diese Felder bereits mit einer Kopie der Felder des Originals gefüllt hat, was für primitive Felder ausreichend ist.  Bei den Feldern, die Referenzen sind, können diejenigen ignoriert werden, die auf unveränderliche Objekte zeigen. Sie sind bereits durch Object.clone() in ausreichender Tiefe kopiert worden, indem die Adresse kopiert wurde.  Dadurch kommt es zwar zu einem Object-Sharing zwischen Klon und Original, weil beide auf dasselbe unveränderliche Objekt verweisen. Aber das ist unproblematisch, weil das gemeinsam referenzierte Objekt sich niemals ändern kann. Es bleiben also nur noch die Felder übrig, die auf veränderliche Objekte verweisen.  Diese Felder müssen nach dem Aufruf von super.clone() in ausreichender Tiefe kopiert werden.
 

Die besonderen Eigenschaften von Object.clone()

Schauen wir uns die Speicherbeschaffung durch Object.clone() noch einmal an.  Kann man den Speicher nicht auch einfach per new beschaffen statt Object.clone() zu rufen? Das funktioniert in der Tat bei Klassen, die als final deklariert sind, macht aber bei einer non-final Klasse keinen Sinn.  Sehen wir uns das fehlerhafte Beispiel einer non-final Klasse an:

class MyClass {
   private int aField;
   ...
   public Object clone() {
     MyClass tmp = new MyClass();
     tmp.aField = aField;
     ... copy all remaining fields to tmp ...
     return tmp;
   }
}

Soweit ist das noch in Ordnung, aber sobald eine Subklasse abgeleitet wird, gibt es Probleme:

class MySubClass extends MyClass {
   private Object anotherField;
   ...
   public Object clone() {
     MySubClass tmp = ... allocate memory ...
     ... copy all fields to tmp ...
     return tmp;
   }
}

Diese Subklasse muss Speicher für den Klon beschaffen und dann noch alle Felder kopiere, um den Klon zur Kopie des Originals zu machen. Dazu müssen nicht nur die eigenen Felder kopiert werden, sondern auch die von der Superklasse geerbten Felder.  Auf die geerbten Felder, die in der Superklasse als private deklariert sind, hat die Subklasse keinen Zugriff, deshalb muss sie an die entsprechende Methode der Superklasse delegieren. Man würde also super.clone() rufen wollen, insbesondere da super.clone() sowieso Speicher beschafft. Hier ist der Versuch eines Aufrufs von super.clone():

class MySubClass extends MyClass {
   private Object anotherField;
   ...
   public Object clone() {
     MySubClass tmp = (MySubClass) super.clone();
     ... copy all fields to tmp ...
     return tmp;
   }
}

Diese Implementierung wird natürlich eine ClassCastException auslösen, weil super.clone() zwar eine Object-Referenz zurück gibt, die aber auf ein Objekt vom Supertyp MyClass verweist. Die Methode MyClass.clone() beschafft zwar Speicher, aber nicht genug; sie erzeugt ein kleineres Objekt, nämlich ein Superklassen-Objekt.  Was wir aber brauchen, ist die Beschaffung von Speicher für das größere Subklassen-Objekt.  Das kann die Superklassen-Methode MyClass.clone() aber nicht leisten, weil die Superklasse keine Kenntnis vom der abgeleiteten Subklasse hat.

Die Methode Object.clone() hingegen hat die besondere Eigenschaft, dass sie Speicher in der richtigen Menge abhängig vom Laufzeittyp des Objekts beschafft und das ganze Objekt bereits als bitweise Kopie des Originals füllt.  Das Ergebnis von Object.clone() ist ein Objekt vom richtigen Typ in der richtigen Größe mit teilweise nützlichem Inhalt.  Weil Object.clone() diese besondere Funktionalität hat, sollte man dafür sorgen, dass diese Methode auch aufgerufen wird, damit sie ihre Aufgaben übernehmen kann.  Man kann sich sicher auch alternative Implementierungen von clone() überlegen, die ohne Object.clone() auskommen und Speicher mit new beschaffen, vielleicht unter Verwendung von Reflection, und irgendwelche Füll-Methoden aus den Superklassen aufrufen.  Verglichen mit dem eleganten Mechanismus über Object.clone() dürfte das allerdings eher ineffizient und umständlich zu warten sein.

Die Regel ist daher: jede potentielle Superklasse, d.h. jede non-final Klasse, muss in ihrer Implementierung von clone() dafür sorgen, dass Speicher für das gesamte Objekt, das von this referenziert wird, beschafft wird, also nicht nur für ein Teil-Objekt vom eigenen Typ.  Und das ist in effizienter Weise nur mit Hilfe von Object.clone() möglich. Deshalb sollte jede Implementierung von clone() als erstes super.clone() aufrufen.

Die non-final Klasse aus unserem obigen Beispiel darf also nicht so aussehen:

class MyClass {
   private int aField;
   ...
   public Object clone() {
     MyClass tmp = new MyClass();
     tmp.aField = aField;
     ... copy all remaining fields to tmp ...
     return tmp;
   }
}

Sondern sie sollte so aussehen:

class MyClass {
   private int aField;
   ...
   public Object clone() {
     try {  MyClass tmp = (MyClass) super.clone(); } catch (CloneNotSupportedException e) {}
     tmp.aField = aField;
     ... copy all remaining fields to tmp ...
     return tmp;
   }
}

Mit dieser Implementierung von clone() lässt sich dann auch die Subklasse übersetzen, weil super.clone() in diesem Falle tatsächlich ein Objekt vom Subklassen-Typ liefert:

class MySubClass extends MyClass {
   private Object anotherField;
   ...
   public Object clone() {
     MySubClass tmp = (MySubClass) super.clone();
     ... copy all fields to tmp ...
     return tmp;
   }
}

Wie man an diesem Beispiel wieder einmal sieht, fällt potentiellen Superklassen, d.h. non-final Klassen, die Verantwortung für die gesamte Subhierarchie zu.  Wenn in einer non-final Klasse clone() falsch implementiert ist, dann haben die Subklassen praktisch keine Chance mehr, eine korrekte Implementierung von clone() zu liefern.  In unserem Beispiel hatte die Superklasse den Speicher für den Klon falsch beschafft, nämlich über new und nicht über Object.clone(). Das ist ein Fehler und führt zu Problemen in den Subklassen-Implementierungen von clone(). Aber es könnte ja auch noch schlimmer sein: was passiert, wenn eine potentielle Superklasse nicht cloneable ist und die Methode clone() überhaupt nicht implementiert?
 

Non-Cloneable Superklassen

Wenn eine non-final Klasse clone() nicht implementiert, dann ist das u.U. eine gravierende Einschränkung für die Subklassen.  Betrachten wir ein Beispiel aus dem JDK: dort gibt es die Klasse java.util.Observable.  Bei einem Observable-Objekt können sich Observer-Objekte registrieren lassen, damit sie später unter bestimmten Umständen notifiziert werden. Ein Observable-Objekt enthält zum Zwecke der Notifizierung ein Array von Observer-Objekten.

Die Klasse Observable ist explizit als Superklasse entworfen, aber sie ist nicht cloneable und sie hat auch keine Implementierung der clone()-Methode. Observable ist also eine non-cloneable Superklasse. Stellen wir uns nun vor, wir wollen von Observable ableiten und die Subklasse soll cloneable sein.

Abbildung 1: Vererbungsbeziehungen für eine cloneable and observable Klasse

Eine Implementierung einer solchen Subklasse könnte wie folgt aussehen:

class Subclass extends java.util.Observable implements Cloneable {
  private Object aField;

  public Object clone() {
    try {
      Subclass tmp = (Subclass) super.clone();
      tmp.aField = aField.clone();
    } catch (CloneNotSupportedException) { ... }
  }
}

Es wird korrekterweise super.clone() gerufen, um den Speicher zu beschaffen und die Superklassen-Anteile in angemessener Tiefe zu kopieren.  Da die Superklasse Observable aber keine Implementierung von clone() hat, ist super.clone() direkt Object.clone().  Es wird nun zwar Speicher für ein Subclass-Objekt beschafft und dieser Speicher wird mit einer bit-weisen Kopie des Original-Subclass-Objekts gefüllt, aber das reicht nicht aus. Die Superklasse Observable enthält ein privates Array von Observer-Objekten und dieses Array wird natürlich nicht kopiert.  Die bit-weise Kopie kopiert lediglich die Adresse des Arrays, so das Original und Klon auf dasselbe Array verweisen.  Das ist keine genügend tiefe Kopie und die Subklasse hat auch keine Chance, eine genügend tiefe Kopie zu erzeugen, weil sie keinen Zugriff auf das private Observer-Array der Superklasse Observable hat.

Das Beispiel zeigt, dass eine Superklasse, die keine clone()-Methode anbietet, ihre Subklassen gravierend einschränkt: die Subklassen können dann u.U. ebenfalls keine clone()-Methode implementieren.

Unter gewissen Umständen muss es nicht zu Problemen führen, wenn eine Superklasse keine clone()-Methode hat.  Wenn etwa alle Felder der Superklasse von primitivem Typ sind, dann ist die von Object.clone() erzeugte bit-weise Kopie bereits genügend tief.  Das gleiche gilt, wenn die Superklasse nur Felder hat, die auf unveränderliche Objekte verweisen. Aber in allen anderen Fällen wirkt sich das Fehlen einer clone()-Methode auf die Subklasse aus: sie kann dann auch nicht cloneable sein.

Fazit: Alle non-final Klassen, die Value-Type [ 2 repräsentieren und nicht-statische Felder haben, welche Referenzen auf veränderliche Objekte oder Arrays von veränderlichen Objekte sind, sollten eine Implementierung von clone() haben, welche super.clone() ruft und alle ihre Felder in genügender Tiefe kopiert.  Diese Implementierung von clone() muss nicht einmal als public Methode angeboten werden und die non-final Klasse muss auch nicht cloneable sein.  Es genügt, wenn eine korrekte Implementierung von clone() als protected Methode zur Verfügung steht, damit Subklassen ihrerseits cloneable werden können und ihre clone()-Methode mit Hilfe der protected clone()-Methode der Superklasse implementieren können.

[2] Die Unterscheidung zwischen Value- und Entity-Typen haben wir bereits in einem vorangegangenen Artikel beschrieben (sieh / KRE2 /), 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.
 

Verwendung von non-final Methoden in clone()


Noch ein Hinweis zur Implementierung von clone() in non-final Klassen: die clone()-Methode sollte keine non-final Methoden rufen. Solche Methoden könnten in Subklassen redefiniert werden und auf subklassen-spezifische Felder zugreifen, die zum Zeitpunkt des Aufrufs noch gar keine sinnvollen Wert haben.

Betrachten wir ein Beispiel: eine non-final Klasse, die in ihrer Implementierung von clone() eine non-final Methode sanityCheck() ruft.  Die Methode sanityCheck() prüft die logische Konsistenz des Objekts und wirft im Fehlerfall die unchecked Exception IllegalStateException, die einfach von clone() einfach durchgelassen wird; aber das ist hier ohne Bedeutung.

class SuperClass implements Cloneable {
  ... private fields ...

  protected void sanityCheck() throws IllegalStateException { ... }

  public Object clone() {
    try { SuperClass tmp = (SuperClass) super.clone(); } catch (CloneNotSupportedException e) {}
    tmp.sanityCheck();
    ... create deep-enough copies of fields ...
    return tmp;
  }
}

Ein Subklasse dieser Klasse wird unter Umständen die non-final Methode sanityCheck() überschreiben und kann dabei auf eigene Felder zugreifen:

class SubClass extend SuperClass {
  private Object aReference;

  ... constructors and stuff ...

  protected void sanityCheck() throws IllegalStateException {
    ... access fields of aReference...
  }
  public Object clone() {
    SubClass tmp = (SubClass) super.clone();
    tmp.sanityCheck();
    tmp.aReference = aReference.clone();
  }
}

Wenn nun SubClass.clone() gerufen wird, dann wird zuerst super.clone(), d.h. SuperClass.clone(), gerufen, welches tmp.sanityCheck() anstößt.  Das Objekt tmp wurde von Object.clone() erzeugt und ist korrekterweise ein SubClass-Objekt.  Es wird daher die Methode SubClass.sanityCheck() gerufen.  Diese Methode greift auf Felder zu, die per aReference zu erreichen sind.  Allerdings hat aReference noch nicht seinen endgültigen Wert zugewiesen bekommen; das geschieht erst im nachfolgenden Statement.  Es ist zu vermuten, dass die Methode sanityCheck()  daher ein fehlerhaftes Ergebnis liefern wird.

Ganz allgemein kann es vorkommen, dass non-final Methoden, die in clone() gerufen werden, den Klon in halbfertigem Zustand antreffen, weil subklassen-spezifische Felder noch nicht  ihren endgültigen Wert haben.  Das ist ein Problem, welches auch bei Konstruktoren auftritt (und das wir hier nicht weiter vertiefen wollen).  Bei Konstruktoren gibt es die Empfehlung, den Aufruf von non-final Methoden zu vermeiden, und das gleiche gilt auch hier.

Fazit : In der Implementierung von clone() vermeide man den Aufruf von non-final Methoden auf dem halbfertigen Klon.
 

Copy-Konstruktion vs. clone()

Im letzten Artikel dieser Kolumne hatten wir neben clone() andere Methoden zur Erzeugung von Kopien von Objekten in Java erwähnt und gesagt, dass die clone())-Methode die empfohlene Technik sei. Sehen wir uns das noch einmal genauer an.

Eine der populärsten dieser alternativen Kopier-Mechanismen ist die Copy-Konstruktion. Klassen mit Copy-Konstruktor haben eine Konstruktor, der ein Objekt vom eigenen Typ als Argument akzeptiert und ein neues Objekt mit gleichem Inhalt erzeugt. Das sieht auf den ersten Blick genauso aus wie die Funktionalität von clone().  Welchen Nachteil hat das gegenüber clone()? Hier ist eine solche Klasse Person, die nicht cloneable ist, aber einen Copy.-Konstruktor hat:

class Person {
  private String name;
  private Date birthday;
  public Person(Person other)
  { name = other.name;
    birthday = (Date)other.birthday.clone();
  }
}

Zunächst einmal ist das völlig in Ordnung.  Die Problem tauchen auf, wenn Subklassen von Person ins Spiel kommen.  Betrachten wir eine Subklasse Employee:

class Employee extends Person {
  private float salary;
  public Employee(Person p, float s)
  { super(p); salary = s; }
  public Employee(Employee other)
  { super(other); salary = other.salary; }
  ...
}

Nehmen wir außerdem an, es gibt eine Methode copy(), welche Person-Objekte kopiert:

Person copy(Person p)
{ return new Person(p); }

Kein Problem, solange nur Person-Objekte kopiert werden.  Die copy()-Methode kann aber auch mit einem Employee als Argument aufgerufen werden.  Dann würde man erwarten, dass als Ergebnis eine Person-Referenz auf ein Employee-Objekt zurück kommt.  Das ist aber nicht der Fall: es wird nur eine Kopie des Person-Anteils des Employee-Objekts geliefert.  Diesen Vorgang der Verstümmelung bezeichnet man als "object slicing" und i.a. allgemeinen ist das kein erwünschter Effekt.  Man erwartet statt dessen, dass copy() ein Objekt von dem Typ zurück liefert, der auch hineingesteckt wurde.

Das Problem des Object-Slicing ließe sich vermeiden, wenn die copy()-Methode abhängig vom Typ des Arguments unterschiedliche Konstruktoren aufrufen würde. Das könnte dann so aussehen:

Person copy(Person p)
{
  if (p.getClass() == Person.class)
     return new Person(p);
  if (p.getClass() == Employee.class)
     return new Employee(p) ;
  ...
}

Allerdings ist das der klassische Wartungsalbtraum: jedes Mal, wenn eine neuen Subklasse entsteht, muss die copy()-Methode um einen weiteren if-Zweig erweitert werden.  So sollte man es auf gar keinen Fall machen.  Für typ-abhängige Methodenaufrufe gibt es in der Objekt-Orientierung den Mechanismus des Polymorphismus. Das Problem ließe sich elegant lösen, indem statt der Kopie per Copy-Konstruktion einen Klon per clone()-Methode erzeugen würde.  Eine bessere Implementierung würde also so aussehen:

Person copy(Person p)
{ return p.clone(); }

Die clone()-Methode ist eine polymorphe Methode, die zur Laufzeit abhängig vom Typ des Objekts, auf dem sie gerufen wird, ausgewählt wird. Wenn also ein Employee als Argument übergeben wird, dann wird automatisch Employee.clone() gerufen, und es wird ein Employee -Objekt und nicht nur ein Person-Objekt erzeugt. Was man für die Implementierung von Methoden wie copy() braucht, ist eine polymorphe Kopier-Methode, und genau das kann ein Konstruktor nicht leisten.  Konstruktoren sind grundsätzlich nicht-polymorphe Methoden. Das polymorphe Kopieren ist nur über clone() zu erreichen.  Aus diesem Grunde ist clone() die bevorzugte Technik für das Kopieren von Objekten. [ 3 ]

Außerdem hat die gezeigte Person-Klasse mit ihrem Copy-Konstruktor die oben schon am Beispiel von Observable diskutierten Schwächen einer non-final Klasse, die nicht cloneable ist und damit ihre Subklassen einschränkt: was auch immer man versucht, es gelingt nicht, die non-final Klasse Employee mit einer korrekten clone()-Methode auszustatten.

[3] Es gibt eine einzige Situation, in der ein Copy-Konstruktor als Ersatz für eine clone()-Methode vertretbar ist:  bei Klassen, die als final deklariert sind.  Eine final Klasse kann keine Subklassen haben; ohne Klassenhierarchie spielt Polymorphie keine Rolle und dann ist auch ein Copy-Konstruktor akzeptabel.
 

clone() und final Felder

Es gibt noch ein Problem bei der Implementierung von clone(): Klassen, die Felder haben, die als final deklariert sind, machen Schwierigkeiten.  Betrachten wir das Beispiel einer Klasse, die ein Feld vom Typ java.util.Date hat, welches eine Zeitstempel enthält.  Der Stempel markiert den Zeitpunkt der letzten Veränderung des Objekts und wird in jeder modifizierenden Methode neu gesetzt.  Aus Optimierungsgründen soll beim Update des Zeitstempels kein neues Date-Objekt erzeugt werden, sondern es soll das vorhandene Date-Objekt geändert werden, um den neuen Zeitstempel abzulegen.  Diese Nutzungsweise des Date-Feldes kann in Java sicher gestellt werden, indem das Feld als final deklariert wird.

Felder oder Variablen, die als final deklariert sind, können nicht verändert werden.  Das bedeutet bei Variablen von primitivem Typ, dass sich der Wert der Variablen niemals ändert.  Bei Referenzvariablen bedeutet es, dass sich die Referenz niemals ändern kann, d.h. die final Referenzvariable wird immer auf dasselbe Objekt verweisen.  Es bedeutet aber nicht, dass das referenzierte Objekt nicht geändert werden kann.

In unserem Beispiel haben wir ein Feld, dass auf ein Date-Objekt verweist.  Das Date-Objekt, d.h. der Zeitstempel, kann geändert werden, aber die Referenz selbst soll aus den oben geschilderten Optimierungsgründen bestehen bleiben.  In einer solchen Situation ist sinnvoll und korrekt, das Referenzfeld als final zu deklarieren, um die gemachte Designentscheidung in der Implementierung zu erzwingen.

Was passiert nun, wenn unsere Klasse mit ihrem Zeitstempel geklont werden soll?  Hier ist der Versuch einer Implementierung von clone() für diese Klasse:

public class MyClass implements Cloneable {
   final private Date cTime = new Date();
   private Integer cInt;

   public Object clone() {
     try { MyClass tmp = (MyClass)super.clone(); }
     catch (CloneNotSupportedException e) {}
     tmp.cTime = new Date();   // error: cannot assign to final field
     tmp.cInt = new Integer(cInt.intValue());
     return tmp;
   }
}

Der Klon soll natürlich seinen eigenen Zeitstempel haben, der zum Zeitpunkt der Erzeugung des Klons mit dem augenblicklichen Zeitpunkt initialisiert wird.  Das wird per Zuweisung versucht, aber leider weist der Compiler die Zuweisung als Fehler zurück, weil final Felder nicht verändert werden können.  Dieses Problem tritt immer auf, wenn Objekte geklont werden sollen, die final Felder haben. Das Problem liegt darin, dass super.clone() bereits ein fertiges Objekt liefert, in dem die final Felder bereits als bit-weise Kopie der entsprechenden Felder des Originals initialisiert sind. final Felder können daher nur den Inhalt haben, den super.clone() ihnen gibt; spätere Änderungen, wie hier die Zuweisung einer Kopie des Date-Objekts, sind nicht möglich.

Für das Problem gibt es nur eine Reihe von denkbaren Lösungen, die aber alle unbefriedigend sind:

  1. Man verzichtet auf die Deklaration von final Felder in Klassen, die cloneable sein sollen. Damit verzichtet man darauf, seine Designentscheidungen klar und deutlich mit Mitteln der Sprache auszudrücken und man macht außerdem etwaige Optimierungen, die der Compiler für final Felder machen könnte, unmöglich.
  2. Man umgeht die final Deklaration mithilfe von Reflection (siehe java.lang.reflect.Field.setAccessible() ).  Das funktioniert aber nur so lange, wie kein Security Manager aktiviert ist, den diesen Hack verhindert. 1
  3. Man greift auf Konstruktoren zurück, um clone() zu implementieren.  Damit handelt man sich aber die oben diskutierten Object-Slicing-Probleme ein.  Eine solche Lösung führt immer dazu, dass die Klasse oder zumindest die clone()-Methode final sein muss.
Eine Lösung per Konstruktor könnte so aussehen:

public class MyClass implements Cloneable {
   final private Date cTime;
   private Integer cInt;

   private MyClass(MyClass m)
   { cTime = new Date(); }

   final public Object clone() {
     MyClass tmp = new MyClass(this);
     tmp.cInt = new Integer(cInt.intValue());
     return tmp;
   }
}

In dieser Lösung verwenden wir ein sogenanntes "blank final" Feld, ein Sprachmittel, dass in den ersten Versionen der Sprache noch gar nicht existierte und erst später (in Version 1.1) eingeführt wurde. Normale final Felder müssen bereits bei der Definition mit ihrem unveränderlichen Wert versorgt werden.  Für solche Felder verlangt der Compiler, dass bereits in der Definition auch der Wert spezifiziert wird, so wie wir das in unserer ersten versuchten Implementierung gemacht hatten. Für blank final Felder ist das etwas anders:  sie müssen nicht schon in der Definition mit ihrem Wert versorgt werden, sondern das kann im Konstruktor geschehen.  Das hat den Vorteil, dass in verschiedenen Konstruktoren verschiedene Werte zugewiesen werden können.  Das war mit den normalen final Feldern nicht möglich; deshalb wurden die blank final Felder erfunden.

Konstruktor gesetzt wird.  In diesem Konstruktor haben wir die Chance das Feld so zu setzen, wie es benötigt wird, nämlich so, dass es auf ein neues Date-Objekt verweist. Diesen privaten Konstruktor verwenden wir in der Implementierung von clone(), damit der Klon seinen eigenen Zeitstempel bekommt.  Der unangenehme Nebeneffekt ist, dass nun der Speicher für den Klon per new  und nicht mit Hilfe von super.clone() beschafft wird, was in Subklassen zu den schon beschriebenen Problemen führt. Deshalb haben wir die clone()-Methode als final deklariert.

Das geschilderte Problem tritt im allgemeinen nur bei final Feldern auf, die Referenzen auf veränderliche Objekte sind.  Bei final Feldern von primitiven Typ und bei final Feldern, die Referenzen auf unveränderliche Objekte sind, ist die von Object.clone() erzeugte bit-weise häufig Kopie bereits ausreichend und eine spätere Zuweisung eines anderen Werts in der clone()-Methode ist nicht nötig.  Damit tritt auch das diskutierte Problem nicht auf.

Wenn das Problem aber auftritt, dann muss man sich zwischen den beiden skizzierten Lösungen entscheiden. Beide Lösungen sind unbefriedigend und man würde sich etwas mehr Unterstützung von der Sprache wünschen. Der Compiler könnte clone()  als "besondere Methode" behandeln und das Setzen von blank final Feldern in clone() erlauben, so wie es auch in den Konstruktoren erlaubt ist.  Aber da das geschilderte Problem in der Praxis nicht extrem häufig auftritt, ist wohl nicht damit zu rechnen, dass sich an der Sprache in dieser Hinsicht etwas ändern wird.
 

Zusammenfassung

Es gibt drei Gründe, warum Klassen eine korrekte Implementierung von clone() haben sollten:
  1. um das polymorphe Kopieren zu ermöglichen. Copy-Konstruktoren leisten dies nicht; die Klasse muss das Cloneable-Interface implementieren und eine public clone()-Methode haben.
  2. um cloneable Subklassen zu ermöglichen. Dazu braucht die Klasse nicht selbst cloneable zu sein; es genügt eine protected clone()-Methode.
  3. um das Kopieren von generischen Collections zu erleichtern. Das hatten wir im letzen Artikel (siehe / KRE1 /) erwähnt. Für jede non.cloneable Klasse muss eine Sonderlösung gefunden werden; für cloneable Klassen ist das Kopieren wesentlich einfacher.
Die einzigen Klassen, die eine clone()-Methode nur aus Grund [3], also nicht unbedingt, brauchen, sind unveränderlichen Klassen wie z.B. String und Klassen, die Entity-Typen repräsentieren, weil man Instanzen von diesen Klassen im Prinzip überhaupt nicht kopieren muss. Sowohl die clone()-Methode als auch ein Copy-Konstruktor ist für solche Klassen optional.

Für Klassen, die als final deklariert sind, gilt Ähnliches:  final Klassen müssen keine Rücksicht auf etwaige Subklassen oder Polymorphie-Anforderungen nehmen.  Falls Kopien gebraucht werden, reicht ein Copy-Konstruktor aus.  Die clone()-Methode würde nur aus Grund [3] gebraucht, was aber bereits Grund genug ist, um die Klasse cloneable zu machen.

Wir haben uns in diesem Artikel außerdem angesehen, worauf man bei der Implementierung von clone() achten muss:

  • Man sollte clone() nicht so deklarieren, dass es eine CloneNotSupportedException wirft. (Dazu mehr im nächsten Artikel).
  • Man sollte in non-final Klassen immer super.clone() aufrufen, um den Speicher für den Klon zu beschaffen.
  • Man sollte in cloneable Klassen final Felder vermeiden, die Referenzen auf veränderliche Objekte sind.

Leserzuschrift

Wir haben im Oktober 2008 dazu folgende Zuschrift erhalten:
 
Leserbrief
 
Hallo,

zum Thema: "Das Kopieren von Objekten in Java Teil 2: Wie implementiert man die clone() Methode?"

stimme ich damit überein, dass beide gebrachten Lösungen zum Handhaben von "final" Feldern beim klonen nicht wirklich zufriedenstellend sind. Es gibt jedoch eine dritte Methode: http://www.javaspecialists.co.za/archive/Issue096.html

Auch wenn die von der verwendeten Java-Version abhängig ist, was auch nicht unproblematisch ist, scheint sie dank dem JSR-133 recht zukunftssicher zu sein. Es bleibt hier das Problem der Threadsafety (wegen dem setAccessible(..)).

Ich denke dennoch, dass es gut wäre, wenn diese Lösung in dem Tutorial auch erwähnt würde, der Vollständigheit halber.

mit freundlichen Grüßen,
Wanja Gayk

Anmerkung:

Wir haben die dritte Lösung per Reflection oben im Text ergänzt. Thread-Safety wäre (entgegen den Äußerungen des Lesers) kein Problem bei dem Workaround per Reflection.  Die Java Language Specification garantiert in Abschnitt 17.5.3 "Subsequent Modification of Final Fields", dass auch Modifikationen von final Feldern außerhalb von Konstruktoren per Reflection von der Initialisation Safety Garantie profitieren und die Modifikationen der final Felder anderen Threads sichtbar werden.  Es kann Probleme zwar mit dem Reordering geben, die aber bei der Implementierung von clone() schwer vorstellbar sind.  Die Lösung per Reflection krankt im wesentlichen daran, dass ein entsprechender Security Manager den Effekt von java.lang.reflect.Field.setAccessible() jederzeit mit Leichtigkeit verhindern kann, womit die "Lösung" nur sehr begrenzt einsetzbar ist.

Literaturverweise

 
/KRE1/ Das Kopieren von Objekten in Java (Teil 1) 
Klaus Kreft & Angelika Langer
JavaSpektrum, September 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/05.Clone-Part1/05.Clone-Part1.html
/KRE3/ Das Kopieren von Objekten in Java (Teil 3): Die CloneNotSupportedException 
Klaus Kreft & Angelika Langer
JavaSpektrum, Januar 2003
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/07.Clone-Part3/07.Clone-Part3.html
/KRE2/ Wie, wann und warum implementiert man die equals()-Methode? (Teil 1)
Klaus Kreft & Angelika Langer
Java Spektrum, Januar 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/01.Equals-Part1/01.Equals1.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
/LIS/  Program Development in Java
Abstraction, Specification, and Object-Oriented Design, Section 4.4
Barbara Liskov with John Guttag 
Addison-Wesley, June 2000 
ISBN: 0-201-65768-6
/JDK/  Java 2 Platform, Standard Edition v1.4.2
URL: http://java.sun.com/j2se/1.4/
/JDOC/  Java 2 Platform, Standard Edition, v 1.4.2 - API Specification
URL: http://java.sun.com/j2se/1.4/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/06.Clone-Part2/06.Clone-Part2.html  last update: 26 Nov 2008