Return-Path: Received: from pacific-carrier-annex.mit.edu by po10.mit.edu (8.9.2/4.7) id MAA26922; Tue, 5 Feb 2002 12:27:58 -0500 (EST) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by pacific-carrier-annex.mit.edu (8.9.2/8.9.2) with SMTP id MAA13007 for ; Tue, 5 Feb 2002 12:27:55 -0500 (EST) Date: Tue, 5 Feb 2002 17:27:56 GMT+00:00 From: "JDC Tech Tips" To: alexp@mit.edu Message-Id: <9110958-517198927@hermes.sun.com> Subject: JDC Tech Tips, February 5, 2002 (Writing toString Methods, Using readResolve) Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-Mailer: Beyond Email J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, February 5, 2002. This issue covers: * Writing toString Methods * Using readResolve These tips were developed using Java(tm) 2 SDK, Standard Edition, v 1.3. You can view this issue of the Tech Tips on the Web at http://java.sun.com/jdc/JDCTechTips/2002/tt0205.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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(tm) Programming Language Third Edition" by Arnold, Gosling, and Holmes http://java.sun.com/docs/books/javaprog/thirdedition/. Also see item 9, Always override toString, in "Effective Java Programming Language Guide" by Joshua Bloch (http://java.sun.com/docs/books/effective/). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING READRESOLVE The August 7, 2001 Tech Tip, "Using Enumerations in Java Programming" (http://java.sun.com/developer/JDCTechTips/2001/tt0807.html#tip2) 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 (http://java.sun.com/docs/books/effective/). . . . . . . . . . . . . . . . . . . . . . . . IMPORTANT: Please read our Terms of Use, Privacy, and Licensing 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, (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to and click "Update". - To unsubscribe, go to the subscriptions page, (http://developer.java.sun.com/subscription/), uncheck the appropriate checkbox, and click "Update". - To use our one-click unsubscribe facility, see the link at the end of this email: - 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 This issue of the JDC Tech Tips is written by Glen McCluskey. 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. To use our one-click unsubscribe facility, select the following URL: http://bulkmail.sun.com/unsubscribe?9110958-517198927