Abstract: Instead of coding the toString() method repeatedly by hand, we present several approaches to generate the String automatically.
Welcome to the 79th edition of The Java(tm) Specialists' Newsletter. My expedition to China was an adventure. At the Beijing Airport, on my way home, I was told that my seat from Singapore to Cape Town had been cancelled due to some technicality. I really did not look forward to being stranded in Singapore for one week, so some serious prayers went up in China and Cape Town. Within one hour, my excellent travel agent managed to get the seat reinstated and confirmed.
Sitting outside on my balcony today, overlooking False Bay (the worlds favourite breeding ground for the Great White Shark), I want to give some free advice if you ever want to purchase a house in Cape Town. We have a strong south-easterly wind that blows most of summer. Looking out over False Bay, I can see "white horses" on the water, which tells me that the wind is fairly strong today. Where we live there is just a gentle breeze. You should look for areas where the wind is not too strong so that you can work outside with your notebook during summer :-)
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
I would like to thank Ravi Nukala for the idea in this newsletter.
This newsletter explores a way in which you can use a generic toString() method that takes any Java object and converts it to a String object, or appends it to a StringBuffer. A restriction in the current system is that circular handles will result in infinite recursion, which cause stack overflow errors in Java. This could perhaps be addressed by keeping a history of which objects had been visited. It complicates the system substantially and this is why I decided not to cater for this.
When I ask a big audience which Design Patterns they know, I usually get at least the following three responses: Singleton, Factory and Facade. Singleton is very comfortable because it seems object-oriented, yet the way that most people use it is not. Factory does not appear "as is" in the Gang-of-Four (GoF, or Erich Gamma et al) book. There is an Abstract Factory and a Factory Method, both of which are very different to the static creation method that is commonly called "The Factory". Then there is the Facade pattern, which I personally maintain is not really a Design Pattern in the classical sense. If you read the book, you will notice that the structure section of the Facade pattern is markedly different to the other patterns. There is a reason for this. The Facade pattern is there to help you sort out the huge number of classes that you get when you use a lot of patterns. The Facade pattern often confused with an interface to a subsystem. The Facade is more than just an interface. It should also allow you direct access to the subsystem, something that an interface would usually not allow. In the sample code of this newsletter, we will see a Facade, that "makes the subsystem easier to use".
Our Facade constructs three Converters and sets them up in a Chain-Of-Responsibility. It then passes each object to the chain, which deals with it by calling the "handle()" method.
/** * The default behaviour is to start with the NullConverter, * followed by the ArrayConverter and then the ObjectConverter. */ public class ToStringFacade { private static final ToStringConverter INITIAL = new NullConverter( new ArrayConverter( new ObjectConverter(null))); public static String toString(Object o) { return toString(o, new StringBuffer()).toString(); } public static StringBuffer toString(Object o, StringBuffer buf) { return INITIAL.handle(o, buf); } }
As a start, we define a Utils class that returns the class name without the package. This utilities class is used in the various converters.
public class Utils { /** We only want to see the class name, without the package. */ public static String stripPackageName(Class c) { return c.getName().replaceAll(".*\\.", "").replace('$', '.'); } }
Next we define an abstract class that we want to use as the main handler in this Chain-of-Responsibility pattern. This avoids having to have a multi-conditional if-else statement that has to decide which Strategy to use for a particular object.
/** * Here we use the Chain-of-Responsibility design pattern that is * very similar to the Strategy pattern, except that we pass the * request on if we cannot deal with it. It helps us to reduce * the number of multi-conditional if-else-if statements that we * need in our program to decide which strategy to use. */ public abstract class ToStringConverter { private final ToStringConverter successor; /** We need to know the successor, if this handler cannot cope * with the request. */ protected ToStringConverter(ToStringConverter successor) { this.successor = successor; } /** handle() decides whether this current object can handle the * request; otherwise it is passed onto the next in the sequence */ protected final StringBuffer handle(Object o, StringBuffer buf) { if (!isHandler(o)) { assert successor != null; return successor.handle(o, buf); } return toString(o, buf); } /** Subclasses can specify whether they can handle the current * object. */ protected abstract boolean isHandler(Object o); /** The toString() method is the main method that is called from * within the handle() method, once it is established which object * should handle the request. */ protected StringBuffer toString(Object o, StringBuffer buf) { buf.append('('); appendName(o, buf); buf.append(getSeparator()); appendValues(o, buf); buf.append(')'); return buf; } /** The separator between name and value can be different for * different types of objects. */ protected char getSeparator() { return '='; } /** This method will determine an identifier for the current object. */ protected void appendName(Object o, StringBuffer buf) { } /** This method will determine the values for the current object. */ protected void appendValues(Object o, StringBuffer buf) { } }
The easiest case to deal with is when the object is null
.
In this case we always let toString() append "(null)":
/** This class follows the Null Object Pattern by Bobby Woolf. */ public class NullConverter extends ToStringConverter { public NullConverter(ToStringConverter successor) { super(successor); } /** This handler is only used if the object is null */ protected boolean isHandler(Object o) { return o == null; } protected StringBuffer toString(Object o, StringBuffer buf) { buf.append("(null)"); return buf; } }
The next case to deal with is when the object is an array. We would like to be able to deal with multi-dimensional arrays. In addition, we want to ensure that we can deal with classes that themselves have not got their own toString() method. We therefore use our own ToStringConverter class unless the array components are primitives, other arrays or Strings.
import java.lang.reflect.Array; /** This converter only supports Arrays. It supports * primitive arrays and multi-dimensional arrays. */ public class ArrayConverter extends ToStringConverter { public ArrayConverter(ToStringConverter successor) { super(successor); } /** This handler only works for arrays. */ protected boolean isHandler(Object o) { return o.getClass().isArray(); } /** We want to append the type of the array and the number of * dimensions, e.g. for a three-dimensional array we want to * append [][][]. Using += for the postfix String is not * ideal, but the most common case will probably be a single * dimension array, in which case there would not be any * concatenation of Strings. */ protected void appendName(Object o, StringBuffer buf) { assert o.getClass().isArray(); String postfix = "[]"; Class c = o.getClass().getComponentType(); while(c.isArray()) { postfix += "[]"; c = c.getComponentType(); } buf.append(Utils.stripPackageName(c)); buf.append(postfix); } /** We show the array using the dimensions and the toString() * methods of the values. This method is recursive to handle multi- * dimensional arrays. */ protected void appendValues(Object o, StringBuffer buf) { assert o.getClass().isArray(); buf.append('{'); int length = Array.getLength(o); for (int i = 0; i < length; i++) { Object value = Array.get(o, i); if (value != null && value.getClass().isArray()) { appendValues(value, buf); } else if (o.getClass().getComponentType().isPrimitive()) { buf.append(value); } else if (value instanceof String) { buf.append('"').append(value).append('"'); } else { ToStringFacade.toString(value, buf); } if (i < length - 1) { buf.append(','); } } buf.append('}'); } }
The most complicated handler is for general objects. We want to show the class name and all the fields and their values. This is done recursively.
import java.lang.reflect.*; import java.util.*; public class ObjectConverter extends ToStringConverter { public ObjectConverter(ToStringConverter successor) { super(successor); } /** This is the end-point of the chain. If we get here, we * use this handler. */ protected boolean isHandler(Object o) { return true; } /** We specify a different separator for between name and * values to make the output look nicer. */ protected char getSeparator() { return ':'; } /** For the name of the class, we strip off the package name. */ protected void appendName(Object o, StringBuffer buf) { buf.append(Utils.stripPackageName(o.getClass())); } /** The values are a bit more complicated. We first have to * find all fields. To find all private fields, we have to * recurse up the hierarchy of the object. We then go through * all the fields and append the name and the value. Unless * we have reached the end, we append ", ". */ protected void appendValues(Object o, StringBuffer buf) { Iterator it = findAllFields(o); while(it.hasNext()) { Field f = (Field) it.next(); appendFieldName(f, o, buf); buf.append('='); appendFieldValue(f, o, buf); if (it.hasNext()) { buf.append(", "); } } } /** If the field's class is not the object's class (i.e. the * field is declared in a superclass) then we print out the * superclass' name together with the field name. */ private void appendFieldName(Field f, Object o, StringBuffer buf) { if (f.getDeclaringClass() != o.getClass()) { buf.append(Utils.stripPackageName(f.getDeclaringClass())); buf.append('.'); } buf.append(f.getName()); } /** We set the field to be "accessible", i.e. public. If the * type of the field is primitive, we simply append the value; * otherwise we recursively print the value using our * ToStringFacade. */ private void appendFieldValue(Field f, Object o, StringBuffer buf) { try { f.setAccessible(true); Object value = f.get(o); if (f.getType().isPrimitive()) { buf.append(value); } else { ToStringFacade.toString(value, buf); } } catch (IllegalAccessException e) { assert false : "We have already set it accessible!"; } } /** Find all fields of an object, whether private or public. * We also look at the fields in super classes. */ private Iterator findAllFields(Object o) { Collection result = new LinkedList(); Class c = o.getClass(); while (c != null) { Field[] f = c.getDeclaredFields(); for (int i = 0; i < f.length; i++) { if (!Modifier.isStatic(f[i].getModifiers())) { result.add(f[i]); } } c = c.getSuperclass(); } return result.iterator(); } }
The last class that we need is the very necessary unit test. This should enable you to refactor the classes to your heart's content, and not be too worried about breaking anything.
import java.util.*; import junit.framework.TestCase; import junit.swingui.TestRunner; public class ToStringTest extends TestCase { public static void main(String[] args) { TestRunner.run(ToStringTest.class); } public ToStringTest(String name) { super(name); } public void testPackageStripping() { assertEquals("Integer", Utils.stripPackageName(Integer.class)); assertEquals("Map.Entry", Utils.stripPackageName(Map.Entry.class)); assertEquals("ToStringTest", Utils.stripPackageName(ToStringTest.class)); } public void testNull() { assertEquals("(null)", ToStringFacade.toString(null)); } public void testInteger() { assertEquals("(Integer:value=42)", ToStringFacade.toString(new Integer(42))); } public void testString() { assertEquals("(String:value=(char[]={H,e,l,l,o, ,W,o,r,l,d,!}), " + "offset=0, count=12, hash=0)", ToStringFacade.toString("Hello World!")); } public void testArray() { assertEquals("(int[]={1,2,3})", ToStringFacade.toString(new int[]{1, 2, 3})); } public void testMultiArray() { assertEquals("(long[][][][]={{{{1,2,3},{4}},{{5}}}})", ToStringFacade.toString( new long[][][][]{{{{1, 2, 3}, {4}}, {{5}}}})); } public void testTestClass() { assertEquals("(ToStringTest.TestClass:names=" + "(String[]={\"Heinz\",\"Joern\",\"Pieter\",\"Herman\"" + ",\"John\"}), totalIQ=900)", ToStringFacade.toString(new TestClass())); } private static class TestClass { private final String[] names = {"Heinz", "Joern", "Pieter", "Herman", "John"}; private final int totalIQ = 180 * 5; } public void testArrayList() { ArrayList list = new ArrayList(); list.add("Heinz"); list.add("Helene"); list.add("Maxi"); list.add("Connie"); assertEquals("(ArrayList:elementData=(Object[]={\"Heinz\"" + ",\"Helene\",\"Maxi\",\"Connie\",(null),(null),(null)," + "(null),(null),(null)}), size=4, AbstractList.modCount=4)", ToStringFacade.toString(list)); } }
That concludes this week's newsletter. My disclaimer this week is that I have not used this approached in a production system. It is probably quite inefficient to do it this way and there could be ways in which you can optimise it. It is sometimes convenient to have a method that will dump the contents of an object, even private fields of the superclasses.
Next week I will show you how it would be possible to have many public classes in one ".java" file, and I will also attempt to convince you why it is not a good idea to do that :-)
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.