Return-Path: <env_9110958-517198927@hermes.sun.com>
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 <alexp@mit.edu>; Tue, 5 Feb 2002 12:27:55 -0500 (EST)
Date: Tue, 5 Feb 2002 17:27:56 GMT+00:00
From: "JDC Tech Tips" <body_9110958-517198927@hermes.sun.com>
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
