- 110 -
magnitude--; return magnitude;
} }
The conversion times compared to the JDK conversions are shown in Table 5-4
. As with
float
s, formatting
double
s with the JDK conversion requires additional steps and would consequently take longer, but the method shown here takes even less time, as you normally print fewer digits that
require fewer loop iterations.
Table 5-4, Time Taken to Append a double to a StringBuffer
VM 1.2 1.3
HotSpot 1.0
1.1.6
JDK double conversion 100 92 129
134 Optimized double conversion
16 16 32
23
5.3.6 Converting Objects to Strings
Converting
Object
s to
String
s is also inefficient in the JDK. For a generic object, the
toString
method is usually implemented by calling any embedded objects
toString
method, then combining the embedded strings in some way. For example,
Vector.toString
calls
toString
on all its elements, and combines the generated substrings with the comma character surrounded by opening and closing square brackets.
Although this conversion is generic, it usually creates a huge number of unnecessary temporary objects. If the JDK had taken the printOn: aStream paradigm from Smalltalk , the temporary
objects used would be significantly reduced. This paradigm basically allows any object to be appended to a stream. In Java, it looks something like:
public String toString {
StringBuffer s =new StringBuffer ; appendTos;
return s.toString ; }
public void appendToStringBuffer s {
The real work of converting to strings. Any embedded objects would have their appendTo methods called,
NOT their toString methods. ...
}
This implementation allows far fewer objects to be created in converting to strings. In addition, as
StringBuffer
is not a stream, this implementation becomes much more useful if you use a
java.io.StringWriter
and change the
appendTo
method to accept any
Writer
, for example:
public String toString {
java.io.StringWriter s =new java.io.StringWriter ; appendTos;
return s.getBuffer .toString ; }
public void appendTojava.io.Writer s {
- 111 -
The real work of converting to strings. Any embedded objects would have their appendTo methods called,
NOT their toString methods. ...
}
This implementation allows the one
appendTo
method to write out any object to any streamed writer object. Unfortunately, this implementation is not supported by the
Object
class, so you need to create your own framework of methods and interfaces to support this implementation. I find that
I can use an
Appendable
interface with an
appendTo
method, and then write
toString
methods that check for that interface:
public interface Appendable {
public void appendTojava.io.Writer s; }
public class SomeClass implements Appendable
{ Object[] embeddedObjects;
... public String toString
{ java.io.StringWriter s =new java.io.StringWriter ;
appendTos; return s.getBuffer .toString ;
} public void appendTojava.io.Writer s
{ The real work of converting to strings. Any embedded
objects would have their appendTo methods called, NOT their toString methods.
for int i = 0; iembeddedObjects.length; i++ if embeddedObjects[i] instanceof Appendable
Appendable embeddedObjects[i].appendTos; else
s.writeembeddedObjects[i].toString ; }
}
In addition, you can extend this framework even further to override the appending of frequently used classes such as
Vector
, allowing a more efficient conversion mechanism that uses fewer temporary objects:
public class AppenderHelper {
final static String NULL = null; final static String OPEN = [;
final static String CLOSE = ]; final static String MIDDLE = , ;
public void appendCheckingAppendableObject o, java.io.Writer s {
Use more efficient Appendable interface if possible, and NULL string if appropriate
if o = v.elementAt0 == null s.writeNULL;
else if o instanceof Appendable
- 112 -
Appendable o.appendTos; else
s.writeo.toString ; }
public void appendVectorjava.util.Vector v, java.io.Writer s {
int size = v.size ; Object o;
Write the opening bracket s.writeOPEN;
if size = 0 {
Add the first element appendCheckingAppendablev.elementAt0, s;
And add in each other element preceded by the MIDDLE separator forint i = 1; i size; i++;
{ s.appendMIDDLE;
appendCheckingAppendablev.elementAti, s; }
} Write the closing bracket
s.appendCLOSE; }
}
If you add this framework to an application, you can support the notion of converting objects to string representations to a particular depth. For example, a
Vector
containing another
Vector
to depth two looks like this:
[1, 2, [3, 4, 5]]
But to depth one, it looks like this:
[1, 2, Vector4444]
The default
Object.toString
implementation in the JDK writes out strings for objects as:
return getClass .getName + + Integer.toHexStringhashCode ;
The JDK implementation is inefficient for two reasons. First, the method creates an unnecessary intermediate string because it uses the concatenation operator twice. Second, the
Class.getName
method which is a
native
method also creates a new string every time it is called: the class name is not cached. It turns out that if you reimplement this to cache the class name and avoid the
extra temporary strings, your conversion is faster and uses fewer temporary objects. The two are related, of course: using fewer temporary objects means less object-creation overhead.
You can create a generic framework that converts the basic data types, while also supporting the efficient conversion of JDK classes such as
Vector
, as well as
Integer
,
Long
, etc.. With this framework in place, I find that performance is generally improved because the application uses
more efficient conversion algorithms and reduces the number of temporary objects. In almost every respect, this framework is better than the simpler framework, which supports only the
toString
method.
- 113 -
5.4 Strings Versus char Arrays