|
Tech Tips Archive
February 05, 2002
WELCOME to the Java Developer Connection (JDC) Tech Tips, February 5, 2002. This issue covers:
These tips were developed using Java 2 SDK, Standard Edition, v 1.3.
This issue of the JDC Tech Tips is written by Glen McCluskey.
WRITING TOSTRING METHODS
One of the standard methods defined in java.lang.Object is toString. This method is used to obtain a string representation of an object. You can (and normally should) override this method for classes that you write. This tip examines some of the issues around using toString.
Let's first consider some sample code:
class MyPoint {
private final int x, y;
public MyPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
public class TSDemo1 {
public static void main(String args[]) {
MyPoint mp = new MyPoint(37, 47);
// use default Object.toString()
System.out.println(mp);
// same as previous, showing the
// function of the default toString()
System.out.println(mp.getClass().getName()
+ "@"
+ Integer.toHexString(mp.hashCode()));
// implicitly call toString() on object
// as part of string concatenation
String s = mp + " testing";
System.out.println(s);
// same as previous, except object
// reference is null
mp = null;
s = mp + " testing";
System.out.println(s);
}
}
|
The TSDemo1 program defines a class MyPoint to represent X,Y points. It does not define a toString method for the class. The program creates an instance of the class and then prints it. When you run TSDemo1, you should see a result that looks something like this:
MyPoint@111f71
MyPoint@111f71
MyPoint@111f71 testing
null testing
You might wonder how it's possible to print an arbitrary class object. The library methods such as System.out.println know nothing about the MyPoint class or its objects. So how is it possible to convert such an object to string form and then print it, as the first output statement in TSDemo1 does?
The answer is that println calls the java.io.PrintStream.print(Object) method, which then calls the String.valueOf method. The String.valueOf method is very simple:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
When println is called with a MyPoint object reference, the String.valueOf method converts the object to a string. String.valueOf first checks to make sure that the reference is not null. It then calls the toString method for the object. Since the MyPoint class has no toString method, the default one in java.lang.Object is used instead.
What does the default toString method actually return as a string value? The format is illustrated in the second print statement above. The name of the class, an "@", and the hex version of the object's hashcode are concatenated into a string and returned. The default hashCode method in Object is typically implemented by converting the memory address of the object into an integer. So your results might vary from those shown above.
The third and fourth parts of the TSDemo1 example illustrate a related idea: when you use "+" to concatenate a string to an object, toString is called to convert the object to a string form. You need to look at the bytecode expansion for TSDemo1 to see that. You can look at the bytecode for TSDemo1 (that is, in a human-readable form) by issuing the javap command as follows:
javap -c . TSDemo1
If you look at the bytecode, you'll notice that part of it involves creating a StringBuffer object, and then using StringBuffer.append(Object) to append the mp object to it. StringBuffer.append(Object) is implemented very simply:
public synchronized StringBuffer append(Object obj) {
return append(String.valueOf(obj));
}
As mentioned earlier, String.valueOf calls toString on the object to get its string value.
O.K., so much for invoking the default toString method. How do you write your own toString methods? It's really very simple. Here's an example:
class MyPoint {
private final int x, y;
public MyPoint(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return x + " " + y;
}
}
public class TSDemo2 {
public static void main(String args[]) {
MyPoint mp = new MyPoint(37, 47);
// call MyPoint.toString()
System.out.println(mp);
// call toString() and
// extract the X value from it
String s = mp.toString();
String t = s.substring(0, s.indexOf(' '));
int x = Integer.parseInt(t);
System.out.println(t);
}
}
|
When you run the TSDemo2 program, the output is:
37 47
37
The toString method in this example does indeed work, but there are a couple of problems with it. One is that there is no descriptive text displayed in the toString output. All you see is a cryptic "37 47". The other problem is that the X,Y values in MyPoint objects are private. There is no other way to get at them except by picking apart the string returned from toString. The second part of the TSDemo2 example shows the code required to extract the X value from the string. Doing it this way is error-prone and inefficient.
Here's another approach to writing a toString method, one that
clears up the problems in the previous example:
class MyPoint {
private final int x, y;
public MyPoint(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return "X=" + x + " " + "Y=" + y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
public class TSDemo3 {
public static void main(String args[]) {
MyPoint mp = new MyPoint(37, 47);
// call MyPoint.toString()
System.out.println(mp);
// get X,Y values via accessor methods
int x = mp.getX();
int y = mp.getY();
System.out.println(x);
System.out.println(y);
}
}
The output is:
X=37 Y=47
37
47
|
This example adds some descriptive text to the output format, and defines a couple of accessor methods to get at the X,Y values. In general, when you write a toString method, the format of the string that is returned should cover all of the object contents. Your toString method should also contain descriptive labels for each field. And there should be a way to get at the object field values without having to pick apart the string. Note that using "+" within toString to build up the return value is not necessarily the most efficient approach. You might want to use StringBuffer instead.
Primitive types in the Java programming language, such as int, also have toString methods, for example Integer.toString(int). What about arrays? How can you convert an array to a string? You can assign an array reference to an Object reference, but arrays are not really classes. However, it is possible to use reflection to implement a toString method for arrays. The code looks like this:
import java.lang.reflect.*;
public class TSDemo4 {
public static String toString(Object arr) {
// if object reference is null or not
// an array, call String.valueOf()
if (arr == null ||
!arr.getClass().isArray()) {
return String.valueOf(arr);
}
// set up a string buffer and
// get length of array
StringBuffer sb = new StringBuffer();
int len = Array.getLength(arr);
sb.append('[');
// iterate across array elements
for (int i = 0; i < len; i++) {
if (i > 0) {
sb.append(',');
}
// get the i-th element
Object obj = Array.get(arr, i);
// convert it to a string by
// recursive toString() call
sb.append(toString(obj));
}
sb.append(']');
return sb.toString();
}
public static void main(String args[]) {
// example #1
System.out.println(toString("testing"));
// example #2
System.out.println(toString(null));
// example #3
int arr3[] = new int[]{
1,
2,
3
};
System.out.println(toString(arr3));
// example #4
long arr4[][] = new long[][]{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println(toString(arr4));
// example #5
double arr5[] = new double[0];
System.out.println(toString(arr5));
// example #6
String arr6[] = new String[]{
"testing",
null,
"123"
};
System.out.println(toString(arr6));
// example #7
Object arr7[] = new Object[]{
new Object[]{null, new Object(), null},
new int[]{1, 2, 3},
null
};
System.out.println(toString(arr7));
}
}
|
The TSDemo4 program creates a toString method, and then passes the toString method an arbitrary Object reference. If the reference is null or does not refer to an array, the program calls the String.valueOf method. Otherwise, the Object refers to an array. In that case, TSDemo4 uses reflection to access the array elements. Array.getLength and Array.get are the key methods that operate on the array. After an element is retrieved, the program calls toString recursively to obtain the string for the element. Doing it this way ensures that multidimensional arrays are handled properly.
The output of the TSDemo4 program is:
testing
null
[1,2,3]
[[1,2,3],[4,5,6],[7,8,9]]
[]
[testing,null,123]
[[null,java.lang.Object@111f71,null],[1,2,3],null]
|
Obviously, if you have a huge array, and you call toString, it will use a lot of memory, and the resulting string might not be particularly useful or readable by a human.
For more information about using toString methods, see Section 2.6.2, Method Invocations, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes. Also see item 9, Always override toString, in "Effective Java Programming Language Guide" by Joshua Bloch.
USING READRESOLVE
The August 7, 2001 Tech Tip, "Using Enumerations in Java Programming" showed an example of what is called a "typesafe enum." Part of the example looks like this:
class EnumColor {
// enumerator name
private final String enum_name;
// private constructor,
//called only within this class
private EnumColor(String name) {
enum_name = name;
}
// return the enumerator name
public String toString() {
return enum_name;
}
// create three enumerators
public static final EnumColor RED =
new EnumColor("red");
public static final EnumColor GREEN =
new EnumColor("green");
public static final EnumColor BLUE =
new EnumColor("blue");
}
|
The example sets up a class with a private constructor, such that no subclassing is possible and no class instances can be created by users of the class. Three instances are created within the class, with each instance used as an enumerator. Using this approach, it's possible to compare enumerators for equality by use of the == operator. There's no issue of violating the type domain as there is with integer enumerations. EnumColor is an example of an "instance-controlled" class. There is guaranteed, for example, to be exactly one EnumColor instance representing EnumColor.GREEN.
Suppose that you need to serialize EnumColor objects, that is, convert them to a stream of bytes and then later reverse the process? How can you do this? Here's one approach:
import java.io.*;
class EnumColor implements Serializable {
// enumerator name
private final String enum_name;
// private constructor
// called only within this class
private EnumColor(String name) {
enum_name = name;
}
// return the enumerator name
public String toString() {
return enum_name;
}
// create three enumerators
public static final EnumColor RED =
new EnumColor("red");
public static final EnumColor GREEN =
new EnumColor("green");
public static final EnumColor BLUE =
new EnumColor("blue");
}
class RRDemo1 {
public static void main(String args[])
throws IOException, ClassNotFoundException {
EnumColor e1 = EnumColor.GREEN;
// serialize
FileOutputStream fos =
new FileOutputStream("test.ser");
BufferedOutputStream bos =
new BufferedOutputStream(fos);
ObjectOutputStream oos =
new ObjectOutputStream(bos);
oos.writeObject(e1);
oos.close();
// deserialize
FileInputStream fis =
new FileInputStream("test.ser");
BufferedInputStream bis =
new BufferedInputStream(fis);
ObjectInputStream ois =
new ObjectInputStream(bis);
EnumColor e2 = (EnumColor)ois.readObject();
ois.close();
// print results
System.out.println("e1 = " + e1);
System.out.println("e2 = " + e2);
// see if e1/e2 refer to the same object
System.out.println(e1 == e2 ? "equal" :
"not equal");
}
}
|
The RRDemo1 program serializes an object representing EnumColor.GREEN, and then deserializes it. When you run the program, the result is:
e1 = green
e2 = green
not equal
Both e1 and e2 have the same setting for the enum_name field (the static fields are not serialized). Unfortunately, these two references do not refer to the same object. So using == to do equality checking fails. The serialization process has broken the property mentioned earlier -- there are now two instances of EnumColor.GREEN, and they can't be compared using ==.
The problem is that the deserialization readObject method always operates on a new class instance. This is true whether readObject is implicitly supplied or whether you write your own for the EnumColor class. So when EnumColor.GREEN is deserialized, it does indeed have the proper setting for the enum_name field, but a new object is generated. Because of this, the whole scheme around controlling EnumColor instances breaks down.
How do you fix this problem? The answer is to use a relatively new serialization feature called readResolve. Here's an example:
import java.io.*;
class EnumColor implements Serializable {
// enumerator name
private final transient String enum_name;
// private constructor
// called only within this class
private EnumColor(String name) {
enum_name = name;
}
// return the enumerator name
public String toString() {
return enum_name;
}
// next index to assign to an enumerator
private static int nextIndex = 0;
// index for this enumerator
private final int index = nextIndex++;
// create three enumerators
public static final EnumColor RED =
new EnumColor("red");
public static final EnumColor GREEN =
new EnumColor("green");
public static final EnumColor BLUE =
new EnumColor("blue");
// table of enumerator values
private static final EnumColor VALUES[] = {
RED,
GREEN,
BLUE
};
// return alternative object
// as result of deserialization
private Object readResolve() throws
ObjectStreamException {
return VALUES[index];
}
}
class RRDemo2 {
public static void main(String args[])
throws IOException, ClassNotFoundException {
EnumColor e1 = EnumColor.GREEN;
// serialize
FileOutputStream fos =
new FileOutputStream("test.ser");
BufferedOutputStream bos =
new BufferedOutputStream(fos);
ObjectOutputStream oos =
new ObjectOutputStream(bos);
oos.writeObject(e1);
oos.close();
// deserialize
FileInputStream fis =
new FileInputStream("test.ser");
BufferedInputStream bis =
new BufferedInputStream(fis);
ObjectInputStream ois =
new ObjectInputStream(bis);
EnumColor e2 = (EnumColor)ois.readObject();
ois.close();
// print results
System.out.println("e1 = " + e1);
System.out.println("e2 = " + e2);
// see if e1/e2 refer to the same object
System.out.println(
e1 == e2 ? "equal" : "not equal");
}
}
|
If you define a readResolve method for a class, it is called on objects of that class after they've been deserialized. The readResolve method can choose to return some other object if it wishes, leaving the deserialized object to be garbage collected.
The RRDemo2 program makes the enum_name field transient. This means that enum_name is not serialized. The program then assigns an index to each enumerator. The index is serialized. readResolve is called after a serialized object (containing only the index) is deserialized. The index is then used to look up in a table of enumerator values, with the appropriate one returned. This scheme preserves the controlled-instance property. The result of running the program is:
e1 = green
e2 = green
equal
The readResolve technique is slightly brittle in that you can't add new enumerators between existing ones. Instead, you have to add them at the end. The serialized form of an enumerator consists of its index. If you change the indexes in EnumColor, then serialized objects will be invalidated.
The readResolve technique is useful when you have instance-controlled classes, such as typesafe enums, singletons, and symbol classes with unique bindings.
For more information about using readResolve, see item 57, Provide a readResolve method when necessary, in "Effective Java Programming Language Guide" by Joshua Bloch.
IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2002 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
JDC Tech Tips
February 5, 2002
Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
|