Java-Tipp 76: Eine Alternative zur Deep-Copy-Technik

Das Implementieren einer tiefen Kopie eines Objekts kann eine Lernerfahrung sein - Sie lernen, dass Sie es nicht tun möchten! Wenn sich das betreffende Objekt auf andere komplexe Objekte bezieht, die sich wiederum auf andere beziehen, kann diese Aufgabe tatsächlich entmutigend sein. Traditionell muss jede Klasse im Objekt einzeln geprüft und bearbeitet werden, um die CloneableSchnittstelle zu implementieren und ihre clone()Methode zu überschreiben , um eine tiefe Kopie von sich selbst und den darin enthaltenen Objekten zu erstellen . Dieser Artikel beschreibt eine einfache Technik, die anstelle dieser zeitaufwändigen herkömmlichen Tiefenkopie verwendet werden kann.

Das Konzept der tiefen Kopie

Um zu verstehen, was eine tiefe Kopie ist, schauen wir uns zunächst das Konzept des flachen Kopierens an.

In einem früheren JavaWorld- Artikel, "So vermeiden Sie Traps und überschreiben Methoden aus java.lang.Object korrekt", erklärt Mark Roulo, wie Objekte geklont werden und wie flaches Kopieren anstelle von tiefem Kopieren erreicht wird. Um es hier kurz zusammenzufassen, eine flache Kopie tritt auf, wenn ein Objekt ohne seine enthaltenen Objekte kopiert wird. Zur Veranschaulichung zeigt Abbildung 1 ein Objekt obj1, das zwei Objekte enthält, containedObj1und containedObj2.

Wenn eine flache Kopie ausgeführt wird obj1, wird sie kopiert, die darin enthaltenen Objekte jedoch nicht, wie in Abbildung 2 dargestellt.

Eine tiefe Kopie tritt auf, wenn ein Objekt zusammen mit den Objekten, auf die es verweist, kopiert wird. Abbildung 3 zeigt, obj1nachdem eine Tiefenkopie durchgeführt wurde. Es wurde nicht nur obj1kopiert, sondern auch die darin enthaltenen Objekte wurden kopiert.

Wenn eines dieser enthaltenen Objekte selbst Objekte enthält, werden diese Objekte in einer tiefen Kopie ebenfalls kopiert usw., bis das gesamte Diagramm durchlaufen und kopiert wird. Jedes Objekt ist dafür verantwortlich, sich über seine clone()Methode selbst zu klonen . Die von clone()ererbte Standardmethode erstellt Objecteine flache Kopie des Objekts. Um eine tiefe Kopie zu erhalten, muss eine zusätzliche Logik hinzugefügt werden, die explizit die clone()Methoden aller enthaltenen Objekte aufruft , die wiederum die clone()Methoden ihrer enthaltenen Objekte aufrufen , und so weiter. Dies richtig zu machen kann schwierig und zeitaufwändig sein und macht selten Spaß. Um die Sache noch komplizierter zu machen: Wenn ein Objekt nicht direkt geändert werden kann und seine clone()Methode eine flache Kopie erzeugt, muss die Klasse erweitert werdenclone()Methode überschrieben, und diese neue Klasse anstelle der alten verwendet. (Enthält beispielsweise Vectornicht die Logik, die für eine tiefe Kopie erforderlich ist.) Wenn Sie Code schreiben möchten, der bis zur Laufzeit die Frage aufschiebt, ob eine tiefe oder flache Kopie eines Objekts erstellt werden soll, müssen Sie noch komplizierter vorgehen Situation. In diesem Fall müssen für jedes Objekt zwei Kopierfunktionen vorhanden sein: eine für eine tiefe Kopie und eine für eine flache. Selbst wenn das tief kopierte Objekt mehrere Verweise auf ein anderes Objekt enthält, sollte das letztere Objekt immer noch nur einmal kopiert werden. Dies verhindert die Verbreitung von Objekten und verhindert die besondere Situation, in der eine Zirkelreferenz eine Endlosschleife von Kopien erzeugt.

Serialisierung

Bereits im Januar 1998 initiierte JavaWorld seine JavaBeans- Kolumne von Mark Johnson mit einem Artikel über die Serialisierung: "Mach es wie im Nescafé - mit gefriergetrockneten JavaBeans." Zusammenfassend ist Serialisierung die Fähigkeit, ein Diagramm von Objekten (einschließlich des entarteten Falls eines einzelnen Objekts) in ein Array von Bytes umzuwandeln, das wieder in ein äquivalentes Diagramm von Objekten umgewandelt werden kann. Ein Objekt gilt als serialisierbar, wenn es oder einer seiner Vorfahren java.io.Serializableoder implementiert java.io.Externalizable. Ein serialisierbares Objekt kann serialisiert werden, indem es an die writeObject()Methode eines ObjectOutputStreamObjekts übergeben wird. Dadurch werden die primitiven Datentypen, Arrays, Zeichenfolgen und anderen Objektreferenzen des Objekts geschrieben. DaswriteObject()Die Methode wird dann für die referenzierten Objekte aufgerufen, um sie ebenfalls zu serialisieren. Ferner werden für jedes dieser Objekte seine Referenzen und Objekte serialisiert. Dieser Vorgang wird fortgesetzt, bis der gesamte Graph durchlaufen und serialisiert wurde. Kommt Ihnen das bekannt vor? Diese Funktionalität kann verwendet werden, um eine tiefe Kopie zu erzielen.

Deep Copy mit Serialisierung

Die Schritte zum Erstellen einer tiefen Kopie mithilfe der Serialisierung sind:

  1. Stellen Sie sicher, dass alle Klassen im Diagramm des Objekts serialisierbar sind.

  2. Erstellen Sie Eingabe- und Ausgabestreams.

  3. Verwenden Sie die Eingabe- und Ausgabestreams, um Objekteingabe- und Objektausgabestreams zu erstellen.

  4. Übergeben Sie das Objekt, das Sie kopieren möchten, an den Objektausgabestream.

  5. Lesen Sie das neue Objekt aus dem Objekteingabestream und setzen Sie es in die Klasse des von Ihnen gesendeten Objekts zurück.

Ich habe eine Klasse namens geschrieben ObjectCloner, die die Schritte zwei bis fünf implementiert. Die mit "A" gekennzeichnete Zeile legt ein fest, mit ByteArrayOutputStreamdem die ObjectOutputStreamZeile B erstellt wird. In Zeile C wird die Magie ausgeführt. Die writeObject()Methode durchläuft rekursiv das Diagramm des Objekts, generiert ein neues Objekt in Byteform und sendet es an das ByteArrayOutputStream. Zeile D stellt sicher, dass das gesamte Objekt gesendet wurde. Der Code in Zeile E erstellt dann ein ByteArrayInputStreamund füllt es mit dem Inhalt von ByteArrayOutputStream. Zeile F instanziiert eine ObjectInputStreamVerwendung der ByteArrayInputStreamin Zeile E erstellten und das Objekt wird deserialisiert und an die aufrufende Methode in Zeile G zurückgegeben. Hier ist der Code:

import java.io.*; import java.util.*; import java.awt.*; public class ObjectCloner { // so that nobody can accidentally create an ObjectCloner object private ObjectCloner(){} // returns a deep copy of an object static public Object deepCopy(Object oldObj) throws Exception { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); // A oos = new ObjectOutputStream(bos); // B // serialize and pass the object oos.writeObject(oldObj); // C oos.flush(); // D ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); // E ois = new ObjectInputStream(bin); // F // return the new object return ois.readObject(); // G } catch(Exception e) { System.out.println("Exception in ObjectCloner = " + e); throw(e); } finally { oos.close(); ois.close(); } } } 

All a developer with access to ObjectCloner is left to do before running this code is ensure that all classes in the object's graph are serializable. In most cases, this should have been done already; if not, it ought to be relatively easy to do with access to the source code. Most of the classes in the JDK are serializable; only the ones that are platform-dependent, such as FileDescriptor, are not. Also, any classes you get from a third-party vendor that are JavaBean-compliant are by definition serializable. Of course, if you extend a class that is serializable, then the new class is also serializable. With all of these serializable classes floating around, chances are that the only ones you may need to serialize are your own, and this is a piece of cake compared to going through each class and overwriting clone() to do a deep copy.

An easy way to find out if you have any nonserializable classes in an object's graph is to assume that they are all serializable and run ObjectCloner's deepCopy() method on it. If there is an object whose class is not serializable, then a java.io.NotSerializableException will be thrown, telling you which class caused the problem.

A quick implementation example is shown below. It creates a simple object, v1, which is a Vector that contains a Point. This object is then printed out to show its contents. The original object, v1, is then copied to a new object, vNew, which is printed to show that it contains the same value as v1. Next, the contents of v1 are changed, and finally both v1 and vNew are printed so that their values can be compared.

import java.util.*; import java.awt.*; public class Driver1 { static public void main(String[] args) { try { // get the method from the command line String meth; if((args.length == 1) && ((args[0].equals("deep")) || (args[0].equals("shallow")))) { meth = args[0]; } else { System.out.println("Usage: java Driver1 [deep, shallow]"); return; } // create original object Vector v1 = new Vector(); Point p1 = new Point(1,1); v1.addElement(p1); // see what it is System.out.println("Original = " + v1); Vector vNew = null; if(meth.equals("deep")) { // deep copy vNew = (Vector)(ObjectCloner.deepCopy(v1)); // A } else if(meth.equals("shallow")) { // shallow copy vNew = (Vector)v1.clone(); // B } // verify it is the same System.out.println("New = " + vNew); // change the original object's contents p1.x = 2; p1.y = 2; // see what is in each one now System.out.println("Original = " + v1); System.out.println("New = " + vNew); } catch(Exception e) { System.out.println("Exception in main = " + e); } } } 

To invoke the deep copy (line A), execute java.exe Driver1 deep. When the deep copy runs, we get the following printout:

Original = [java.awt.Point[x=1,y=1]] New = [java.awt.Point[x=1,y=1]] Original = [java.awt.Point[x=2,y=2]] New = [java.awt.Point[x=1,y=1]] 

This shows that when the original Point, p1, was changed, the new Point created as a result of the deep copy remained unaffected, since the entire graph was copied. For comparison, invoke the shallow copy (line B) by executing java.exe Driver1 shallow. When the shallow copy runs, we get the following printout:

Original = [java.awt.Point[x=1,y=1]] New = [java.awt.Point[x=1,y=1]] Original = [java.awt.Point[x=2,y=2]] New = [java.awt.Point[x=2,y=2]] 

This shows that when the original Point was changed, the new Point was changed as well. This is due to the fact that the shallow copy makes copies only of the references, and not of the objects to which they refer. This is a very simple example, but I think it illustrates the, um, point.

Implementation issues

Now that I've preached about all of the virtues of deep copy using serialization, let's look at some things to watch out for.

The first problematic case is a class that is not serializable and that cannot be edited. This could happen, for example, if you're using a third-party class that doesn't come with the source code. In this case you can extend it, make the extended class implement Serializable, add any (or all) necessary constructors that just call the associated superconstructor, and use this new class everywhere you did the old one (here is an example of this).

This may seem like a lot of work, but, unless the original class's clone() method implements deep copy, you will be doing something similar in order to override its clone() method anyway.

The next issue is the runtime speed of this technique. As you can imagine, creating a socket, serializing an object, passing it through the socket, and then deserializing it is slow compared to calling methods in existing objects. Here is some source code that measures the time it takes to do both deep copy methods (via serialization and clone()) on some simple classes, and produces benchmarks for different numbers of iterations. The results, shown in milliseconds, are in the table below:

Milliseconds to deep copy a simple class graph n times
Procedure\Iterations(n) 1000 10000 100000
clone 10 101 791
serialization 1832 11346 107725

As you can see, there is a large difference in performance. If the code you are writing is performance-critical, then you may have to bite the bullet and hand-code a deep copy. If you have a complex graph and are given one day to implement a deep copy, and the code will be run as a batch job at one in the morning on Sundays, then this technique gives you another option to consider.

Another issue is dealing with the case of a class whose objects' instances within a virtual machine must be controlled. This is a special case of the Singleton pattern, in which a class has only one object within a VM. As discussed above, when you serialize an object, you create a totally new object that will not be unique. To get around this default behavior you can use the readResolve() method to force the stream to return an appropriate object rather than the one that was serialized. In this particular case, the appropriate object is the same one that was serialized. Here is an example of how to implement the readResolve() method. You can find out more about readResolve() as well as other serialization details at Sun's Web site dedicated to the Java Object Serialization Specification (see Resources).

One last gotcha to watch out for is the case of transient variables. If a variable is marked as transient, then it will not be serialized, and therefore it and its graph will not be copied. Instead, the value of the transient variable in the new object will be the Java language defaults (null, false, and zero). There will be no compiletime or runtime errors, which can result in behavior that is hard to debug. Just being aware of this can save a lot of time.

The deep copy technique can save a programmer many hours of work but can cause the problems described above. As always, be sure to weigh the advantages and disadvantages before deciding which method to use.

Conclusion

Das Implementieren einer tiefen Kopie eines komplexen Objektgraphen kann eine schwierige Aufgabe sein. Die oben gezeigte Technik ist eine einfache Alternative zum herkömmlichen Verfahren zum Überschreiben der clone()Methode für jedes Objekt in der Grafik.

Dave Miller ist leitender Architekt bei der Beratungsfirma Javelin Technology, wo er an Java- und Internetanwendungen arbeitet. Er hat für Unternehmen wie Hughes, IBM, Nortel und MCIWorldcom an objektorientierten Projekten gearbeitet und in den letzten drei Jahren ausschließlich mit Java gearbeitet.

Erfahren Sie mehr über dieses Thema

  • Die Java-Website von Sun enthält einen Abschnitt, der der Java Object Serialization Specification gewidmet ist

    //www.javasoft.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html

Diese Geschichte "Java-Tipp 76: Eine Alternative zur Deep-Copy-Technik" wurde ursprünglich von JavaWorld veröffentlicht.