Java Tip 76: An alternative to the deep copy technique
Tutorial Details:
Java Tip 76: An alternative to the deep copy technique
Java Tip 76: An alternative to the deep copy technique
By: By Dave Miller
Use serialization to make deep copies and avoid extensive manual editing or extending of classes
mplementing a deep copy of an object can be a learning experience -- you learn that you don't want to do it! If the object in question refers to other complex objects, which in turn refer to others, then this task can be daunting indeed. Traditionally, each class in the object must be individually inspected and edited to implement the Cloneable interface and override its clone() method in order to make a deep copy of itself as well as its contained objects. This article describes a simple technique to use in place of this time-consuming conventional deep copy.
The concept of deep copy
In order to understand what a deep copy is, let's first look at the concept of shallow copying.
In a previous JavaWorld article, " How to avoid traps and correctly override methods from java.lang.Object ," Mark Roulo explains how to clone objects as well as how to achieve shallow copying instead of deep copying. To summarize briefly here, a shallow copy occurs when an object is copied without its contained objects. To illustrate, Figure 1 shows an object, obj1 , that contains two objects, containedObj1 and containedObj2 .
Figure 1. The original state of obj1
If a shallow copy is performed on obj1 , then it is copied but its contained objects are not, as shown in Figure 2.
Figure 2. After a shallow copy of obj1
A deep copy occurs when an object is copied along with the objects to which it refers. Figure 3 shows obj1 after a deep copy has been performed on it. Not only has obj1 been copied, but the objects contained within it have been copied as well.
Figure 3. After a deep copy of obj1
If either of these contained objects themselves contain objects, then, in a deep copy, those objects are copied as well, and so on until the entire graph is traversed and copied.
Each object is responsible for cloning itself via its clone() method. The default clone() method, inherited from Object , makes a shallow copy of the object. To achieve a deep copy, extra logic must be added that explicitly calls all contained objects' clone() methods, which in turn call their contained objects' clone() methods, and so on. Getting this correct can be difficult and time consuming, and is rarely fun. To make things even more complicated, if an object can't be modified directly and its clone() method produces a shallow copy, then the class must be extended, the clone() method overridden, and this new class used in place of the old. (For example, Vector does not contain the logic necessary for a deep copy.) And if you want to write code that defers until runtime the question of whether to make a deep or shallow copy an object, you're in for an even more complicated situation. In this case, there must be two copy functions for each object: one for a deep copy and one for a shallow. Finally, even if the object being deep copied contains multiple references to another object, the latter object should still only be copied once. This prevents the proliferation of objects, and heads off the special situation in which a circular reference produces an infinite loop of copies.
Serialization
Back in January of 1998, JavaWorld initiated its JavaBeans column by Mark Johnson with an article on serialization, " Do it the 'Nescafé' way -- with freeze-dried JavaBeans ." To summarize, serialization is the ability to turn a graph of objects (including the degenerate case of a single object) into an array of bytes that can be turned back into an equivalent graph of objects. An object is said to be serializable if it or one of its ancestors implements java.io.Serializable or java.io.Externalizable . A serializable object can be serialized by passing it to the writeObject() method of an ObjectOutputStream object. This writes out the object's primitive data types, arrays, strings, and other object references. The writeObject() method is then called on the referred objects to serialize them as well. Further, each of these objects have their references and objects serialized; this process goes on and on until the entire graph is traversed and serialized. Does this sound familiar? This functionality can be used to achieve a deep copy.
Deep copy using serialization
The steps for making a deep copy using serialization are:
Ensure that all classes in the object's graph are serializable.
Create input and output streams.
Use the input and output streams to create object input and object output streams.
Pass the object that you want to copy to the object output stream.
Read the new object from the object input stream and cast it back to the class of the object you sent.
I have written a class called ObjectCloner that implements steps two through five. The line marked "A" sets up a ByteArrayOutputStream which is used to create the ObjectOutputStream on line B. Line C is where the magic is done. The writeObject() method recursively traverses the object's graph, generates a new object in byte form, and sends it to the ByteArrayOutputStream . Line D ensures the whole object has been sent. The code on line E then creates a ByteArrayInputStream and populates it with the contents of the ByteArrayOutputStream . Line F instantiates an ObjectInputStream using the ByteArrayInputStream created on line E and the object is deserialized and returned to the calling method on line G. Here's the 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 wh
Read
Tutorial at: Click here to view the tutorial
Rate Tutorial: Java Tip 76: An alternative to the deep copy technique
View Tutorial: Java Tip 76: An alternative to the deep copy technique
Related
Tutorials:
Applet to Applet
Communication
Applet to Applet
Communication |
Java Tip 56: How to
eliminate debugging problems for RMI-based applications - JavaWorld - July 1998
Java Tip 56: How to
eliminate debugging problems for RMI-based applications - JavaWorld - July 1998 |
Java Tip 39: The trick
to using a basic
Java 1.1 network
and file class
loader - JavaWorld -
Java Tip 39: The trick
to using a basic
Java 1.1 network
and file class
loader - JavaWorld - October
1997 |
Java Tip 71: Use
dynamic messaging in Java - JavaWorld - April
1999
Java Tip 71: Use
dynamic messaging in Java - JavaWorld - April
1999 |
Java Tip 76: An alternative to the deep copy technique
Java Tip 76: An alternative to the deep copy technique |
Java Tip 77: Enable copy and paste functionality
between Swing's JTables and Excel - JavaWorld
Java Tip 77: Enable copy and paste functionality
between Swing's JTables and Excel - JavaWorld |
How to avoid traps and correctly override methods from java.lang.Object - JavaWorld January 1999
How to avoid traps and correctly override methods from java.lang.Object - JavaWorld January 1999 |
Java Tip 84: Customize
scoping with object keys - JavaWorld
Java Tip 84: Customize
scoping with object keys - JavaWorld |
Speed up listener
notification - JavaWorld February 2000
Speed up listener
notification - JavaWorld February 2000 |
Dynamic user interface is
only skin deep - JavaWorld May
2000
Dynamic user interface is
only skin deep - JavaWorld May
2000 |
Alternative deployment
methods, Part 2: The best of both worlds -
JavaWorld July
2000
Alternative deployment
methods, Part 2: The best of both worlds -
JavaWorld July
2000 |
Reduce EJB
network traffic with astral clones - JavaWorld December
2000
Reduce EJB
network traffic with astral clones - JavaWorld December
2000 |
C#: A language alternative or just J--?, Part 2 - JavaWorld December 2000
C#: A language alternative or just J--?, Part 2 - JavaWorld December 2000 |
Attack of the
clones
Attack of the
clones |
roots of
constants classes
roots of
constants classes |
Unwrap the package statement's
potential
Unwrap the package statement's
potential |
Datastructures and algorithms, Part 1
Datastructures and algorithms, Part 1 |
Sizeof for
Java
Sizeof for
Java |
Java theory and practice: Generics gotchas
Generic types, added in JDK 5.0, are a significant enhancement to type safety in the Java language. However, some aspects of generics may seem confusing, or even downright bizarre, to first-time users. In this month's Java theory and practice, Brian Goetz |
ORA
ORA is a Framework written in Java (plattform independent). |
|
|
|