|
|||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||
|
Implementing the clone() Method - Part 1
|
||||||||||||||||||||||||||||
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 {
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 SharingDas 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 ObjektenFü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()-MethodeWenn 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 {
Kopieren per Copy-KonstruktorWenn 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 {
Andere Formen des KopierensDaneben gibt es Klassen, die werden copy-konstruierbar noch cloneable sind. Die JKD-Klasse java.lang.StringBuffer ist ein solches Beispiel:
public final class StringBuffer {
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()-ContractDie 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.
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 {
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.
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 CopyIn 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) };
try {
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);
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) };
try {
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"?
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:
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.
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 CollectionsDas 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 {
public Object clone() {
for (int i=0; i<oa.length; i++)
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:
...
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 ReflectionIm 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:
...
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 CollectionsBeim 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++)
else if (oa[i] instanceof String)
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 ObjektenGrundsä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 AusblickIn 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
|
|||||||||||||||||||||||||||||
© 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 |