|
Tech Tips Archive
March 6, 2001
WELCOME to the Java Developer Connection sm(JDC) Tech Tips, March 6, 2001. This issue covers:
This tip was developed using Java 2 SDK, Standard Edition, v 1.3.
This issue of the JDC Tech Tips is written by Glen McClusky.
Suppose you have some objects that you're using in an application. How can you make copies of the objects? The most obvious approach is to simply assign one object to another, like this:
obj2 = obj1;
But this approach actually does no copying of the objects; instead the approach only copies the object references. In other words, after you perform this operation, there is still only one object, but now there is an additional reference to the object.
If this seemingly obvious approach doesn't work, how do you actually clone an object? Why not try the method Object.clone? This method is available to all of Object's subclasses. Here's an attempt:
class A {
private int x;
public A(int i) {
x = i;
}
}
public class CloneDemo1 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
}
}
|
This code triggers a compile error, because Object.clone is a protected method.
So let's try again, with another approach:
class A {
private int x;
public A(int i) {
x = i;
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo2 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
}
}
|
In this approach, you define your own clone method, which extends Object.clone. The CloneDemo2 program compiles, but gives a CloneNotSupportedException when you try to run it.
There's still a piece missing. You have to specify that the class containing the clone method implements the Cloneable interface, like this:
class A implements Cloneable {
private int x;
public A(int i) {
x = i;
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
public int getx() {
return x;
}
}
public class CloneDemo3 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
System.out.println(obj2.getx());
}
}
|
Success! CloneDemo3 compiles and produces the expected result:
37
You've learned that you must explicitly specify the clone method, and your class must implement the Cloneable interface. Cloneable is an example of a "marker" interface. The interface itself specifies nothing. However, Object.clone checks whether a class implements it, and if not, throws a CloneNotSupportedException.
Object.clone does a simple cloning operation, copying all fields from one object to a new object. In the CloneDemo3 example, A.clone calls Object.clone. Then Object.clone creates a new A object and copies the fields of the existing object to it.
There are a couple of other important points to consider about the approach illustrated in the CloneDemo3 example. One is that you can prevent a user of your class from cloning objects of the class. To do this, you don't implement Cloneable for the class, and the clone method always throws an exception. Most of the time, however, it's better to explicitly plan for and implement a clone method in your class so that objects are copied appropriately.
Another point is that you can support cloning either unconditionally or conditionally. The code in the CloneDemo3 example supports cloning unconditionally, and the clone method
does not propagate CloneNotSupportedException.
A more general approach is to conditionally support cloning for a class. In this case, objects of the class itself can be cloned, but objects of subclasses possibly cannot be cloned. For conditional cloning, the clone method must declare that it can propagate CloneNotSupportedException. Another example of conditional support for cloning is where a class is a collection class whose objects can be cloned only if the collection elements can be cloned.
Yet another approach is to implement an appropriate clone method in a class, but not implement Cloneable. In that case, subclasses can support cloning if they wish.
Cloning can get tricky. For example, because Object.clone does a simple object field copy, it sometimes might not be what you want. Here's an example:
import java.util.*;
class A implements Cloneable {
public HashMap map;
public A() {
map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo4 {
public static void main(String args[]) {
A obj1 = new A();
A obj2 = (A)obj1.clone();
obj1.map.remove("key1");
System.out.println(obj2.map.get("key1"));
}
}
|
You might expect CloneDemo4 to display the result:
value1
But instead it displays:
null
What's happening here? In CloneDemo4, A objects contain a HashMap reference. When A objects are copied, the HashMap reference is also copied. This means that an object clone contains the original reference to the HashMap object. So when a key is removed from the HashMap in the original object, the HashMap in the copy is updated as well.
To fix this problem, you can make the clone method a bit more sophisticated:
import java.util.*;
class A implements Cloneable {
public HashMap map;
public A() {
map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
}
public Object clone() {
try {
A aobj = (A)super.clone();
aobj.map = (HashMap)map.clone();
return aobj;
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo5 {
public static void main(String args[]) {
A obj1 = new A();
A obj2 = (A)obj1.clone();
obj1.map.remove("key1");
System.out.println(obj2.map.get("key1"));
}
}
|
The Clone5Demo example displays the expected result:
value1
Clone5Demo calls super.clone to create the A object and copy the map field. The example code then calls HashMap.clone to do a special type of cloning peculiar to HashMaps. This operation consists of creating a new hash table and copying entries to it from the old one.
If two objects share a reference, as in CloneDemo4, then you're likely to have problems unless the reference is read-only. To get around the problems, you need to implement a clone method that handles this situation. Another way of saying it is that Object.clone does a "shallow" copy, that is, a simple field-by-field copy. It doesn't do a "deep" copy, where each object referred to by a field or array is itself recursively copied.
It's extremely important to call super.clone, instead of, for example, saying "new CloneDemo5" to create an object. You should call super.clone at each level of the class hierarchy. That's because each level might have its own problems with shared objects. If you use "new" instead of "super.clone", then your code will be incorrect for any subclass that extends your class; the code will call your clone method and receive an incorrect object type in return.
One other thing to know about cloning is that it's possible to clone any array, simply by calling the clone method:
public class CloneDemo6 {
public static void main(String args[]) {
int vec1[] = new int[]{1, 2, 3};
int vec2[] = (int[])vec1.clone();
System.out.println(vec2[0]
+ " " + vec2[1] +
" " + vec2[2]);
}
}
|
A final important point about cloning: it's a way to create and initialize new objects, but it's not the same as calling a constructor. One example of why this distinction matters concerns blank finals, that is, uninitialized fields declared "final", that can only be given a value in constructors. Here's an example of blank final usage:
public class CloneDemo7 {
private int a;
private int b;
private final long c;
public CloneDemo7(int a, int b) {
this.a = a;
this.b = b;
this.c = System.currentTimeMillis();
}
public static void main(String args[]) {
CloneDemo7 obj = new CloneDemo7(37, 47);
}
}
|
In the CloneDemo7 constructor, a final field "c" is given a timestamp value that is obtained from the system clock. What if you want to copy an object of this type? Object.clone copies all the fields, but you want the timestamp field to be set to the current system clock value. However, if the field is final, you can only set the field in a constructor, not in a clone method. Here's a illustration of this issue:
public class CloneDemo8 {
private int a;
private int b;
private final long c;
public CloneDemo8(int a, int b) {
this.a = a;
this.b = b;
this.c = System.currentTimeMillis();
}
public CloneDemo8(CloneDemo8 obj) {
this.a = obj.a;
this.b = obj.b;
this.c = System.currentTimeMillis();
}
public Object clone() throws
CloneNotSupportedException {
//this.c = System.currentTimeMillis();
return super.clone();
}
public static void main(String args[]) {
CloneDemo8 obj = new CloneDemo8(37, 47);
CloneDemo8 obj2 = new CloneDemo8(obj);
}
}
|
If you uncomment the line that attempts to set final field "c", the program won't compile. So instead of using clone to set the field, the program uses a copy constructor. A copy constructor is a constructor for the class that takes an object reference of the same class type, and implements the appropriate copying logic.
You might think you could solve this problem by not using blank finals, and instead simply use final fields that are immediately initialized with the system time, like this:
class A implements Cloneable {
final long x = System.currentTimeMillis();
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo9 {
public static void main(String args[]) {
A obj1 = new A();
// sleep 100 ms before doing clone,
// to ensure unique timestamp
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
System.err.println(e);
}
A obj2 = (A)obj1.clone();
System.out.println(obj1.x + " " + obj2.x);
}
}
|
This doesn't work either. When you run the program, you see that obj1.x and obj2.x have the same value. This indicates that normal object initialization is not done when an object is cloned, and you can't set the value of final fields within the clone method. So if simple copying is not appropriate for initializing a field, you can't declare it final. Or else you need to use copy constructors as a cloning alternative.
For more information about object cloning, see Section 3.9, Cloning Objects, and Section 2.5.1, Constructors, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes.
Serialization is a mechanism in which arbitrary objects can be converted into a byte stream, to be saved to disk or transmitted across a network. The byte stream can later be deserialized to reconstitute the object. You make the default serialization mechanism available simply by declaring that your class implements the java.io.Serializable marker interface.
This tip presents a two-part example that uses what is called the Serializable Fields API. If you've worked with serialization, you know that it's possible to override the default mechanism for a given class. One way you can do this is by defining your own readObject and writeObject methods. The Serializable Fields API ties in to this mechanism.
Imagine a simple application where you would like to keep a cumulative total of the number of weeks that have elapsed from some starting point. You save this information in object form to a file, and periodically add to the total. Here's a program that does this:
import java.io.*;
class ElapsedTime implements Serializable {
static final long serialVersionUID =
892420644258946182L;
private double numweeks;
// read a serialized object
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields =
in.readFields();
numweeks = fields.get("numweeks", 0.0);
}
// write a serialized object
private void writeObject(ObjectOutputStream
out)
throws IOException {
ObjectOutputStream.PutField fields =
out.putFields();
fields.put("numweeks", numweeks);
out.writeFields();
}
// constructor
public ElapsedTime() {
numweeks = 0.0;
}
// add to the elapsed time
public void addTime(double t) {
numweeks += t;
}
// return the elapsed time
public double getTime() {
return numweeks;
}
}
public class FieldDemo1 {
private static final String DATAFILE =
"data.ser";
public static void main(String args[])
throws IOException, ClassNotFoundException {
if (args.length != 1) {
System.err.println("missing command
line argument");
System.exit(1);
}
ElapsedTime et = null;
// read serialized object
// if data file exists,
// else create a new object
if (new File(DATAFILE).exists()) {
FileInputStream fis = new
FileInputStream(DATAFILE);
ObjectInputStream ois = new
ObjectInputStream(fis);
et = (ElapsedTime)ois.readObject();
fis.close();
}
else {
et = new ElapsedTime();
}
// update the elapsed time
et.addTime(Double.parseDouble(args[0]));
// write the serialized object
FileOutputStream fos = new
FileOutputStream(DATAFILE);
ObjectOutputStream oos =
new ObjectOutputStream(fos);
oos.writeObject(et);
oos.close();
System.out.println("Elapsed time is " +
et.getTime() + " weeks");
}
}
|
The FieldDemo1 program reads a serialized object of class ElapsedTime from the data file (data.ser). If the file doesn't exist, the program creates a default object. Then the program updates the cumulative time, and writes the object back to the file. You can use the program like this:
$ javac FieldDemo1.java
$ java FieldDemo1 10
$ java FieldDemo1 20
This sequence adds two values (10 and 20) to the cumulative total of elapsed weeks.
The program's use of serialization is straightforward, except for two things. The first is the serialVersionUID field (this will be discussed further a little later in the tip). The second thing is the use of the Serializable Fields API.
The FieldDemo1 program defines readObject and writeObject methods. These methods interact with serialization by specifying field names on which to operate. For example, one of the calls is:
numweeks = fields.get("numweeks", 0.0);
This interface is quite different than the usual mechanism, where the fields of the object are set for you. Using a custom readObject method and the Serializable Fields API provide an alternative so that you can specify fields by name. The fields can be accessed in any order. Also, the default value specified to the get method is used if the input stream does not contain an explicit value for the field.
The interface used in writeObject is similar. The program uses putFields to set up a buffer, and then it can put serializable field values into the buffer in any order. The program uses writeFields to write the buffer to the stream. Any fields that have not been set are given default values (0, false, null) appropriate to the field type.
Note that the use of the Serializable Fields API in the above example is not required. It offers a different type of interface that helps to make clear exactly what is going on with the setting of the various fields. But there are tradeoffs here, for example, in performance. Using this API when you don't need to, might not always be a good idea.
Suppose that you've been using the above approach for a while in your application, and then you decide to represent elapsed time as a number of days instead of weeks. It's pretty easy to change the ElapsedTime class to do this. But what happens to all the ElapsedTime objects that have been serialized and are sitting around in databases and files?
Suppose too, for the moment, that your original version of ElapsedTime did not declare the static "serialVersionUID" field. If you change ElapsedTime, then when you try to deserialize old objects of this class, you get an InvalidClassException. This is because serialization uses what is known as a "serial version UID" to detect compatible versions of a given class. The serial version UID is a 64-bit number whose default value (if not explicitly declared) is a hash of the class name, interface names, member signatures, and miscellaneous class attributes. The serial version UID for a class is written when instances of the class are serialized. If the class changes, the default serial version UID value changes as well. When ObjectInputStream deserializes objects, it checks to make sure that serial version UIDs contained in the incoming stream match those of the classes loaded in the receiving JVM*. If a mismatch occurs, then an InvalidClassException is thrown.
If you change a class, but you don't want the serialization mechanism to complain, you need to declare your own serialVersionUID field in the class. If the earlier version of the class did not already declare a serial version UID value, then you can determine its implicit (default) serial version UID value by using the serialvertool, which is included in the standard JDK distribution:
$ javac FieldDemo1.java
$ serialver -classpath . ElapsedTime
If the previous version of the class already explicitly declared a serial version UID value, then you can simply leave the declaration in place with the same value.
As a general rule, it's always a good idea to declare explicit serial version UID values for serializable classes. There are two reasons for doing that:
-
Runtime computation of the serial version UID hash is expensive in terms of performance. If you don't declare a serial version UID value, then serialization must do it for you at runtime.
-
The default serial version UID computation algorithm is extremely sensitive to class changes, including those that might result from legal differences in compiler implementations. For instance, a given serializable nested class might have different default serial version UID values depending on which
javac was used to compile it. That's because javac must add synthetic class members to implement nested classes, and differences in these synthetic class members are picked up by the serial version UID calculation.
Let's look at the second demo program, and then finish the explanation:
import java.io.*;
class ElapsedTime implements Serializable {
static final long serialVersionUID =
892420644258946182L;
// list of fields to be serialized; this list
// need not match the actual fields in
// ElapsedTime
private static final ObjectStreamField
serialPersistentFields[] = {
new ObjectStreamField("numweeks",
Double.TYPE)
};
// transient (unserialized)
//field for this object
private transient double numdays;
// read a serialized object
private void readObject(ObjectInputStream in)
throws IOException,
ClassNotFoundException {
ObjectInputStream.GetField fields =
in.readFields();
numdays = fields.get("numweeks", 0.0) *
7.0;
}
// write a serialized object
private void writeObject(ObjectOutputStream
out)
throws IOException {
ObjectOutputStream.PutField fields =
out.putFields();
fields.put("numweeks", numdays / 7.0);
out.writeFields();
}
// constructor
public ElapsedTime() {
numdays = 0.0;
}
// add to the elapsed time
public void addTime(double t) {
numdays += t;
}
// get the elapsed time
public double getTime() {
return numdays;
}
}
public class FieldDemo2 {
private static final String DATAFILE =
"data.ser";
public static void main(String args[])
throws IOException,
ClassNotFoundException {
if (args.length != 1) {
System.err.println("missing command
line argument");
System.exit(1);
}
ElapsedTime et = null;
// read serialized object if it exists, else
// create a new object
if (new File(DATAFILE).exists()) {
FileInputStream fis = new
FileInputStream(DATAFILE);
ObjectInputStream ois = new
ObjectInputStream(fis);
et = (ElapsedTime)ois.readObject();
fis.close();
}
else {
et = new ElapsedTime();
}
// add to the elapsed time
et.addTime(Double.parseDouble(args[0]));
// write the serialized object
FileOutputStream fos = new
FileOutputStream(DATAFILE);
ObjectOutputStream oos = new
ObjectOutputStream(fos);
oos.writeObject(et);
oos.close();
System.out.println("Elapsed time is " +
et.getTime() + " days");
}
}
|
Both programs have the serialVersionUID field set to the same value, so serialization will consider the two ElapsedTime class versions to be compatible with each other.
But there's a problem here. If elapsed time was represented as a number of weeks, but now as a number of days, then the field numweeks in the serialized objects would simply be wrong. You could change it to days, but that makes all the existing objects invalid.
The solution to this problem is to keep the original representation in the serialized objects, but use the Serializable Field API to translate between days and weeks. That is, continue to read and write objects containing numweeks, but then multiply or divide by 7.0 to give the number of days.
Notice that numdays is the field used in the new version of the ElapsedTime class. But this is not the same as the field in actual serialized objects, which is numweeks. If you try to put fields by name, using the Serializable Fields API, and the name doesn't actually represent a serializable field in the class, you get an exception. So you need to go a step further, by adding the following declaration to the ElapsedTime class:
private static final ObjectStreamField
serialPersistentFields[] = {
new ObjectStreamField("numweeks",
Double.TYPE)
};
|
This is a special field that the serialization mechanism knows about. By setting this field, you can override the default set of serializable fields. The fields in this list do not have to be part of the current class definition for the class that you're trying to serialize. The ObjectStreamField constructor takes a field name and a field type, with the type represented using java.lang.Class.
So what you've done is serialize numweeks, and declared numdays as transient, that is, not subject to serialization. Using this approach, it is possible for both old and new versions of the ElapsedTime class to read and write serialized objects. In other words, you can read existing objects using the new class version, write out updated objects, and then use the old class version to read and write them as well.
Here's an example sequence that illustrates this idea (before you try this sequence, be sure to remove any copy of data.ser that you have around):
javac FieldDemo1.java
java FieldDemo1 10
javac FieldDemo2.java
java FieldDemo2 14
javac FieldDemo1.java
java FieldDemo1 5
javac FieldDemo2.java
java FieldDemo2 28
|
When the sequence is executed, the result is:
Elapsed time is 10.0 weeks
Elapsed time is 84.0 days
Elapsed time is 17.0 weeks
Elapsed time is 147.0 days
This output demonstrates that both object views (weeks and days) are represented. For example, the first program adds 10 weeks to the total, or 70 days. Then the second program is called to add 14 days, and the total is now 84 days.
For more information about using the Serializable Fields API, see the tutorial "Using Serialization and the Serializable Fields API" (http://java.sun.com/j2se/1.3/docs/guide/serialization/examples/altimpl/index3.html), and the Java Object Serialization Specification
(http://java.sun.com/j2se/1.3/docs/guide/serialization/spec/serialTOC.doc.html).
- NOTE
Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun Microsystems purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click the Update button.
- SUBSCRIBE
To subscribe to a JDC newsletter mailing list, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to, and click Update.
- FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
- LINKS TO NON-SUN SITES
The JDC Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource.
This issue of the JDC Tech Tips is written by Glen McCluskey.
JDC Tech Tips
March 6, 2001
* As used in this document, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.
Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
|