AngelikaLanger.com - Implementing the clone() Method - Part 3 (original) (raw)
Implementing the clone() Method - Part 3
Das Kopieren von Objekten in Java Teil 3: Die CloneNotSupportedException - Sollte die clone()-Methode eine CloneNotSupportedException werfen? JavaSPEKTRUM, Januar 2003 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). |
---|
In dieser Ausgabe unserer Kolumne wollen wir uns ansehen, ob eine clone()-Methode eine CloneNotSupportedException werfen sollte. Wir haben uns in den vorangegangenen zwei Artikeln (siehe /KRE1/) bereits ausführlich mit clone() beschäftigt und dabei vorgeschlagen, dass clone() grundsätzlich keine CloneNotSupportedException werfen sollte. Dieses Thema wird aber in der Java-Community durchaus kontrovers diskutiert und wir wollen uns aus diesem Grunde den JDK genauer ansehen, um zu sehen, woher diese kontroverse Diskussion eigentlich stammt.
Die Idee von Cloneable, clone() und der 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 manche Programmierer für die clone()-Methoden ihrer eigenen Klassen die Signatur von Object.clone(), nämlich
Object clone() throws CloneNotSupportedException;
Nun ist es 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 haben wir empfohlen, die clone()-Methode immer als Methode zu deklarieren, die keine CloneNotSupportedException wirft.
Das ist auch im JDK gängige Praxis. Fast alle Klassen des JDK, die das Cloneable-Interface implementieren, haben eine clone()-Methode, die keine CloneNotSupportedException wirft. Die Verwirrung rührt im wesentlichen von der JavaDoc-Beschreibung der Methode Object.clone() her. Hier ein Auszug aus der Original-Beschreibung:
Throws:
CloneNotSupportedException - if the object's class does not support the Cloneable interface. Subclasses that override the clone method can also throw this exception to indicate that an instance cannot be cloned.
OutOfMemoryError - if there is not enough memory.
Manche Java-Programmierer haben das so verstanden, dass alle clone()-Methoden die CloneNotSupportedException in ihrer throws-Klausel deklarieren sollten, damit Subklassen die Möglichkeit haben, diese Exception zu werfen. Schließlich kann man ja als Autor einer Superklasse nicht wissen, ob Subklassen später überhaupt die clone()-Methode implementieren können oder wollen.
Diese Argumentation ist insoweit richtig, als die Deklaration von clone() ohne throws-Klausel in einer non-final Klasse tatsächlich bedeutet, dass eine Subklasse nicht die Freiheit hat, in ihrer Implementierung von clone() irgendwelche checked Exceptions zu werfen. Diese Einschränkung ist aber eigentlich auch in Ordnung. Bei einem sauberen objekt-orientierten Design repräsentiert die Ableitungsbeziehung zwischen Super- und Subklasse eine sogenannte "is-a"-Beziehung, d.h. ein Objekt der Subklasse ist vom Typ her kompatibel zu einem Superklassenobjekt und kann überall dort verwendet werden, wo ein Objekt der Superklasse verlangt wird. Dieses Prinzip ist als Liskov Substitution Principle (LSP) bekannt (siehe u.a. /LIS/).
Das bedeutet insbesondere, dass die Subklasse sämtliche Operationen unterstützen muss, die die Superklasse unterstützt. Nun kann das Werfen einer CloneNotSupportedException in der clone()-Methode kaum als "Unterstützen der clone()-Operation" bezeichnet werden; es ist vielmehr das Gegenteil. Unter solchen Umständen entstünde eine Klasse, die cloneable wäre (da sie das Cloneable-Interface von der Superklasse erbt) und eine clone()-Methode hätte, aber dann beim Aufruf eine CloneNotSupportedException werfen würde. Das ist gegen jede Intuition und etwa so unlogisch wie eine Klasse, die cloneable ist, aber keine clone()-Methode hat.
Nun kann es aber vorkommen, dass die Subklasse tatsächlich keinen Klon erzeugen kann. Das kann zum Beispiel passieren, wenn kein Speicher mehr vorhanden ist. Was macht man mit solchen oder anderen Fehlersituationen? Das wirft ganz allgemein die Frage auf: wie kann das Scheitern einer clone()-Methode zum Ausdruck gebracht werden, wenn die clone()-Methode so deklariert ist, dass sie keine Exception wirft? Nun, durch eine checked Exception geht es offensichtlich nicht. Das ist aber auch richtig so. Exceptions (sowohl checked als auch unchecked Exceptions) drücken in Java logische Fehler aus, die vorhersehbar und vermeidbar sind, im Gegensatz zu den Errors, die schwere Ausnahmezustände beschreiben, die auf Fehler in der Laufzeitumgebung (Virtuelle Maschine, Garbage Collector, AWT) zurückgehen.
Wenn bereits von der Logik her klar ist, dass in bestimmten Situationen kein Klon erzeugt werden kann, dann sollte die Klasse schon von vornherein das Cloneable-Interface gar nicht implementieren und auch keine clone()-Methode haben. Schließlich ist das Cloneable-Interface für jeden Benutzer der Klasse genau das Kennzeichen, an dem man erkennen kann, dass die Klasse cloneable ist, und dann sollte die Klasse auch nur dann cloneable sein, wenn sich die clone()-Methode sinnvoll implementieren lässt.
Unvorhersehbare Fehler können natürlich trotzdem auftreten. Das sind dann aber schwere Ausnahmefehler, die durch einen Error ausgedrückt werden, und keine "CloneNotSupported"-Situationen. Der Mangel an Speicherplatz ist ein Beispiel; in solchen Fällen wird ein OutOfMemoryError ausgelöst. Andere Fehlersituationen sind eigentlich kaum vorstellbar, wenn sich alle Klassen an die Regel halten, dass sie keine CloneNotSupportedException werfen, wenn sie cloneable sind. Das wird klar, wenn man sich ansieht, was eine kanonische Implementierung von clone() tut: sie ruft die clone()-Methoden für alle Felder und die Superklasse auf. Die einzige clone()-Methode, die eine CloneNotSupportedException werfen könnte, ist Object.clone(). Aber genau das kann nicht eintreten, weil die Klasse cloneable ist.
Fazit: Die clone()-Methode von Klassen, die das Cloneable-Interface implementieren, sollte keine Exception werfen. Alle denkbaren Fehlersituationen sind so schwere Fehler, dass sie angemessen über einen Error ausgedrückt werden.
Sehen wir uns nach diesen Betrachtung jetzt einmal an, wie die Klassen aus dem JDK ihre clone()-Methoden implementieren.
Implementierungen von clone() im JDK
Praktisch alle clone()-Methoden im JDK folgen der oben beschriebenen Regel. Es gibt allerdings einen häufig auftretenden Fehler: viele clone()-Methoden sind in der JavaDoc so beschrieben, dass sie eine CloneNotSupportedException werfen. Wenn man dann die Implementierung dieser Methoden anschaut, stellt man fest: es stimmt gar nicht. Die Methoden haben korrekterweise keine throws-Klausel und werfen auch keine Exceptions. Da passen Implementierung und Dokumentation ganz offensichtlich nicht zusammen.
Das Phänomen erklärt sich dadurch, dass für diese Methoden keine JavaDoc-Kommentare geschrieben wurden. In solchen Fällen benutzt das JavaDoc-Tool automatisch die Beschreibung der Methode der Superklasse. Die Beschreibung aus der Superklasse ist in all diesen Fällen unpassend: es ist nämlich die Beschreibung von Object.clone(). Dieser offensichtliche Dokumentationsfehler sollte uns daran erinnern, dass man ihn für seine eigenen Klassen leicht vermeiden kann, indem man zu jeder Methode, die man implementiert, auch tatsächlich JavaDoc-Kommentare schreibt.
Von diesem Dokumentationsfehler mal abgesehen, sind aber praktisch alle clone()-Methoden so implementiert, dass sie keine Exception, und damit insbesondere keine CloneNotSupportedException, werfen. Die meisten dieser Implementierungen rufen super.clone()auf, und damit letztendlich Object.clone(), und müssen irgendwie mit der CloneNotSupportedException fertig werden, die für Object.clone() deklariert ist, aber gar nicht auftreten kann, weil die Klasse das Cloneable-Interface implementiert. Für den Umgang mit der CloneNotSupportedException, die gar nicht auftreten kann, findet man drei verschiedene Strategien im JDK: "völlig unterdrücken" oder "abbilden auf einen InternalError" oder "null zurückgeben". Sehen wir uns diese drei Strategien einmal anhand von Beispielen näher an.
CloneNotSupportedException unterdrücken
Ein Beispiel für diese Implementierungstechnik findet man zum Beispiel in java.util.Date:
public Object clone() {
Date d = null;
try {
d = (Date)super.clone();
if (d.cal != null) d.cal = (Calendar)d.cal.clone();
} catch (CloneNotSupportedException e) {} // Won't happen
return d;
}
Die Klasse Date ist direkt von Object abgeleitet und implementiert das Cloneable-Interface, deshalb kann von super.clone() keine CloneNotSupportedException kommen. Calendar.clone() ist so deklariert, dass es keine Exception wirft; hier kann also auch keine CloneNotSupportedException auftreten. Deshalb wird die CloneNotSupportedException abgefangen und unterdrückt.
CloneNotSupportedException abbilden auf einen InternalError
Ein Beispiel für diese Implementierungstechnik findet man zum Beispiel in java.awt.geom.Point2D:
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}
Das ist im Prinzip die gleiche Situation wie oben bei Date. Hier hat der Autor aber entschieden, dass die CloneNotSupported Exception, nicht völlig unterdrückt werden soll, sondern dass dies ein interner Fehler ist. Ist dieser InternalError gerechtfertigt? Irgendwie schon. Wenn von Object.clone() tatsächlich eine CloneNotSupportedException kommt, was eigentlich nicht sein kann, dann liegt in der Tat in der Laufzeitumgebung ein schweres Problem vor: vielleicht eine inkonsistente oder fehlerhafte virtuelle Maschine oder eine andere kaum vorstellbare Fehlersituation.
CloneNotSupportedException abbilden auf die Rückgabe einer null-Referenz
Das ist eine eher exotische Variante, die wir in der Klasse java.text.Format gefunden haben:
public Object clone() {
try {
Format other = (Format) super.clone();
return other;
} catch (CloneNotSupportedException e) {
// will never happen
return null;
}
}
So geht es natürlich auch. Hier wird der Returnwert der Methode verwendet, um die Fehlersituation zum Ausdruck zu bringen. Das ist ein schönes Beispiel, welches die Grauzone zwischen Returncodes und Exceptions demonstriert. Unter Umständen kann man dieselbe logische Information entweder über einen besonderen Fehler-Returncode oder über eine Exception ausdrücken kann. Man hätte auch die Methode Object.clone() so spezifizieren können, dass sie ganz ohne Exception auskommt. Object.clone() hat zwei mögliche Ergebnisse: die Referenz auf den erzeugten Klon, falls dieser erzeugt werden konnte, oder die Information, dass das Objekt nicht cloneable ist. Das letztere Ergebnis hätte sich in einer null-Referenz als Rückgabewert ausdrücken lassen. Das hat man allerdings anders gemacht; es wird statt dessen die CloneNotSupportedException geworfen. Und deshalb ist die oben gezeigte Variante einer clone()-Implementierung auch wernig empfehlenswert; eigentlich rechnet kein Benutzer mit einer null-Referenz als Ergebnis von clone().
Empfehlenswert sind die Varianten "Unterdrücken" und" InternalError". Welche von beiden Techniken man vorzieht, ist Geschmacksache. Man kann natürlich auch einen anderen Error oder gar eine unchecked Exception werfen. Beides ist aber unüblich. Es hat sich eingebürgert, dass man einen InternalError wirft, wenn man die CloneNotSupportedException nicht unterdrücken will.
Das leere Cloneable-Interface
Die ganze Verwirrung um die CloneNotSupportedException hätte sich von vornherein vermeiden lassen, wenn das Cloneable-Interface klare Vorgaben machen würde. Die Tatsache, dass Cloneable ein leeres Interface ist, hat allerlei Nachteile.
Wir haben schon im vorletzten Artikel gesehen, dass das leere Cloneable-Interface zum Beispiel beim Kopieren von generischen Collections Schwierigkeiten bereitet; es bleibt einem nichts anderes übrig, als die clone()-Methode per Reflection aufzurufen, weil der Cast auf Cloneable keinen Zugriff auf die clone()-Methode gibt. Außerdem kann es Klassen geben, die das Cloneable-Interface implementieren, aber keine clone()-Methode haben, was völlig widersinnig ist, aber nicht verhindert werden kann. Und im Zusammenhang mit der CloneNotSupportedException wäre es auch wünschenswert, dass das Cloneable-Interface sinnvolle Vorgaben über eine throws-Klausel für die clone()-Methode machte.
Besteht die Aussicht, dass das Cloneable-Interface vielleicht in Zukunft korrigiert wird? Wohl kaum. Egal wie man die clone()-Methode eines Cloneable-Interfaces definiert, die Korrektur würde existierenden Code brechen. Da es nie Vorgaben für die Signatur von clone() gegeben hat, existieren clone()-Methoden mit und ohne throws-Klausel. (Es gibt sogar clone()-Methoden mit throws(CloneNotSupportedException)-Klausel im JDK. Ein Beispiel ist die Klasse java.awt.datatransfer.DataFlavor.)
Ganz egal, wie man sich bei der Korrektur von Cloneable entscheidet, ein Teil des heute existierenden Java-Codes würde unübersetzbar werden. Wenn man das Cloneable-Interface mit einer clone()-Methode ohne throws-Klausel definiert, dann werden all die Klassen unbrauchbar, die eine clone()-Methode mit "throws CloneNotSupportedException "-Klausel haben. Wenn man umgekehrt das Cloneable-Interface mit einer clone()-Methode mit "throws CloneNotSupportedException"-Klausel definiert, dann blieben zwar alle cloneable Klassen gültig, aber die Benutzer dieser Klassen haben ein Problem: sie müssen plötzlich die CloneNotSupportedException behandeln, wenn sie nach eine Cast auf Cloneable die clone()-Methode aufrufen. Wie auch immer man das anstellt, die Änderung des heutigen leeren Cloneable-Interfaces würde in jedem Fall existierenden Code brechen. Solche Brüche hat Sun bislang vermieden; man ist dort sehr um Kompatibilität der JDK-Versionen bemüht. Deshalb ist nicht zu erwarten, dass das Cloneable-Interface jemals eine clone()-Methode haben wird.
Nun kann man das für eigene Projekte und Klassen natürlich anders und besser machen. Als wir das Für und Wider der CloneNotSupportedException auf der OOP-Konferenz im Januar 2002 dargestellt haben, kam folgender Vorschlag aus dem Auditorium: "Kann man nicht ein projekt-spezifisches Cloneable-Subinterface haben, dass eine clone()-Methode hat und diese Interface anstelle des Cloneable-Interfaces verwenden?" Das ist eine gute Idee, die natürlich voraussetzt, dass es Programmierrichtlinien gibt oder die Software-Entwickler anderweitig motiviert sind, dieses neue Interface auch zu verwenden. Das Interface könnte dann wie folgt aussehen:
/**
* In contrast to the standard interface java.lang.Cloneable
* this interface has a clone
method. It is supposed to be
* used in lieu of the standard java.lang.Cloneable
interface.
*
* @author ...
* @version ...
* @see java.lang.Cloneable
*/
public interface CloneableWithCloneMethod extends Cloneable {
/**
* Creates and returns a copy of this object.
*
* @return a clone of this instance.
* @exception OutOfMemoryError in case of not enough memory.
* @exception InternalError in case of an unexpected CloneNotSupportedException.
* @see project.CloneableWithCloneMethod
*/
public Object clone();
}
Damit ist man zwar für die eigenen Klassen einen Schritt weiter, aber beim Klonen von generischen Collections beispielsweise muss man sich immer noch mit existierenden third-party Klassen herumschlagen, die das Cloneable-Interface implementieren und von dem projektspezifischen CloneableWithCloneMethod-Interface nichts wissen.
Zusammenfassung
Die clone()-Methode sollte keine CloneNotSupportedexception werfen, sondern im Fehlerfall einen InternalError auslösen.
Literaturverweise
/KRE1/ | Das Kopieren von Objekten in Java (Teil 1 + II) Klaus Kreft & Angelika LangerJavaSpektrum, September + November 2002URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/05.Clone-Part1/05.Clone-Part1.html URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/06.Clone-Part2/06.Clone-Part2.html |
---|---|
/LIS/ | Program Development in Java Abstraction, Specification, and Object-Oriented Design, Section 4.4Barbara 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) |