A Generic toString in Java

Alternative to Java's toString() method: A generic toString in java

For the impatient readers who know the problem I am trying to solve and need the code can scroll down to the Source code section. If you are still reading then, let us describe the problem in hand.

Problem Statement

Have you ever faced an issue where you included a POJO(Plain Old Java Object) in your source code and tried to print it's state in the logs or may be on the console. But unlike your expectations you ended up with some ugly object references which gives no good representation of the data you require. This thing gets worse when the POJO is a part of some sealed jar where we do not have any control over the toString() method of the POJO. It becomes much more worse, if we have control over the POJO's source code but eventually have some one in the team who keeps on changing the definition and does not change the toString() method.

Solution

I tried writing a generic solution where I am not forced to reach out to all my POJO's and individually override them. I also wanted to get rid of frequent modifications of the POJO and missing out essential information while printing the state of the object.

Trade Offs

For getting this much of guarantee from the source code which relieves you of all the worries of subsequent modifications and inaccessibility to the source code, it definitely requires to employ Java's Reflection. Hence, there is a slight performance overhead. But I believe in the world of 3+ GHz i5 and i7 processors and 8-16 gigs or RAM, this is acceptable.

Please read the class level comment to understand the options.

package org;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Generic Utility to be used as an alternative to toString method. This utility
 * can print the toString method of any object ( even the one's without a
 * suitable overriden toString() method )
 * 
 * This also provides few options as below:
 * Ignore describing collections.
 * Ignore describing the arrays.
 * Ignore printing the type information of each field.
 * 
 * 
 * The default variant will print all the collections & arrays and all the type
 * information. Also included a sample usage in the main method.
 * 
 * 
 * @author dprasad
 * 
 */
public class Describe {

	boolean ignoreCollections;
	boolean ignoreArrays;
	boolean ignoreType;

	public Describe(boolean ignoreCollections, boolean ignoreArrays, boolean ignoreType) {
		this.ignoreArrays = ignoreArrays;
		this.ignoreCollections = ignoreCollections;
		this.ignoreType = ignoreType;
	}

	public Describe() {
		this.ignoreArrays = false;
		this.ignoreCollections = false;
		this.ignoreType = false;
	}

	String describeInner(Object o, StringBuilder sb) {
		
		if (o instanceof Collection) {
			if (!ignoreCollections) {
				sb.append(" [ ");

				for (Object i : (Collection) o) {
					describeInner(i, sb);
					sb.append(" , ");
				}
				
				sb.replace(sb.length() - 2, sb.length(), "");
				sb.append("] ");
			} 
			
			else {
				sb.append("ignored collection");
			}
		} 
		
		else if (o instanceof Map) {
			if (!ignoreCollections) {
				sb.append(" [");
				
				for (Object i : ((Map) o).entrySet()) {
					if (i instanceof Entry) {
						describeInner(((Entry) i).getKey(), sb);
						sb.append(" : ");
						describeInner(((Entry) i).getValue(), sb);
						sb.append(", ");
					}
				}

				sb.replace(sb.length() - 2, sb.length(), "");
				sb.append("] ");
			} else {
				sb.append("ignored collection");
			}
		} 
		
		else if (o.getClass().isArray()) {
			if (!ignoreArrays) {
				sb.append(" [");
				
				for (Object i : (Object[]) o) {
					describeInner(i, sb);
					sb.append(", ");
				}
				
				sb.replace(sb.length() - 2, sb.length(), "");
				sb.append("] ");
			} else {
				sb.append("ignored arrays");
			}
		} 
		
		else if (o instanceof Number || o instanceof Character
				|| o instanceof Boolean || o instanceof String) {
			sb.append(o);
		} 
		
		else {
			sb.append(reflect(o));
		}
		
		return null;
	}

	private String reflect(Object o) {
		Field[] fields = o.getClass().getDeclaredFields();
		StringBuilder s = new StringBuilder();
		s.append("Object{");
		
		for (Field f : fields) {
			s.append(f.getName());
			s.append("=(");
		
			try {
				f.setAccessible(true);
				if (!ignoreType) {
					String type = f.getGenericType().toString();
					s.append(type);
					s.append(", ");
				}
				Object value = f.get(o);

				describeInner(value, s);
				s.append(")");
			} catch (IllegalArgumentException | IllegalAccessException e) {
				e.printStackTrace();
			}
			
			s.append(", ");
		}
		s.replace(s.length() - 2, s.length(), "");
		s.append("} ");
		return s.toString();
	}

	public String describe(Object o) {
		if (o != null) {
			StringBuilder sb = new StringBuilder();
			describeInner(o, sb);
			return sb.toString();
		}
		return null;
	}

	public static void main(String[] args) {
		TestObject r = new TestObject();
		Describe d1 = new Describe(true, true, false);
		Describe d2 = new Describe(true, false, false);
		Describe d3 = new Describe(false, false, false);
		Describe d4 = new Describe(true, true, true);
		System.out.println(d1.describe(r));
		System.out.println(d2.describe(r));
		System.out.println(d3.describe(r));
		System.out.println(d4.describe(r));
	}
}
  Note: The code above does not take into account the Enum types and the properties in the parent class (in case of an hierarchical object relationship. Below is another piece of the code enhanced enough to pick up enum types and fields from parent class which are not in the framework library. For example: If we have a class hierarchy A extends B extends Date, then this utility prints the properties of A and B and the value of Date field if needed, but it doesn't print all the properties of Date and GregorianCalendar and Object.
package com.ua.meta;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Generic Utility to be used as an alternative to toString method. This utility
 * can print the toString method of any object ( even the one's without a
 * suitable overriden toString() method )
 * 
 *
 * This also provides few options as below:
 * Ignore describing collections.
 * Ignore describing the arrays.
 * Ignore printing the type information of each field.
 *
 * 
 *
 * The default variant will print all the collections & arrays and all the type 
 * information. Also included a sample usage in the main method. 
 *
 * 
 * @author dprasad 
 * */ 
 public class Describe { 
    boolean ignoreCollections; 
	boolean ignoreArrays; 
	boolean ignoreType; 
	
	public Describe(boolean ignoreCollections, boolean ignoreArrays, boolean ignoreType) 
	{ 
	   this.ignoreArrays = ignoreArrays; 
	   this.ignoreCollections = ignoreCollections; 
	   this.ignoreType = ignoreType; 	   
	} 
	
	public Describe() { 
	   this.ignoreArrays = false; 
	   this.ignoreCollections = false; 
	   this.ignoreType = false; 
	} 
	
	String describeInner(Object o, StringBuilder sb) { 
	   if (o instanceof Collection) { 
	      if (!ignoreCollections) { 
		    sb.append(" ["); 
			for (Object i : (Collection) o) { 
			    describeInner(i, sb); 
				sb.append(", "); 
			} 
			sb.replace(sb.length() - 2, sb.length(), ""); 
			sb.append("] "); 
		} else { 
		  sb.append("ignored collection"); 
		} 
      } else if (o instanceof Map) { 
	     if (!ignoreCollections) { 
		    sb.append(" ["); 
			for (Object i : ((Map) o).entrySet()) { 
			   if (i instanceof Entry) { 
			      describeInner(((Entry) i).getKey(), sb); 
				  sb.append(" : "); 
				  describeInner(((Entry) i).getValue(), sb); sb.append(", "); 
			   } 
			 } 
			 sb.replace(sb.length() - 2, sb.length(), ""); 
			 sb.append("] "); 
	     } else { 
		    sb.append("ignored collection"); 
		 } 
	 } else if (o.getClass().isArray()) { 
	    if (!ignoreArrays) { 
		   sb.append(" ["); 
		   for (Object i : (Object[]) o) { 
		      describeInner(i, sb); 
			  sb.append(", "); 
		   } 
		   sb.replace(sb.length() - 2, sb.length(), ""); 
		   sb.append("] "); 
		} else { 
		   sb.append("ignored arrays"); 
		} 
     } else if (o instanceof Number || o instanceof Character || o instanceof Boolean || o instanceof String) { 
	    sb.append(o); 
	 } else if( o instanceof Enum){ 
	    sb.append(((Enum) o).name()); 
	 } else { 
	    sb.append(reflect(o)); 
	 } 
	 return null; 
 } 
 
  private List getAllFieldsRec(Class clazz, List list) { 
     Class superClazz = clazz.getSuperclass(); 
	 if(superClazz != null && !isFrameworkClass(superClazz)){ 
	    getAllFieldsRec(superClazz, list); 
	 } 
	 list.addAll(Arrays.asList(clazz.getDeclaredFields())); 
	 return list; 
  } 
  
  private boolean isFrameworkClass(Class clazz){ 
     if(clazz.getName().startsWith("java.")) 
	    return true; 
	 if(clazz.getName().startsWith("sun.")) 
	    return true; 
	 if(clazz.getName().startsWith("org.")) 
	    return true; 
	 return false; 
  } 
  
  private String reflect(Object o) { 
     StringBuilder s = new StringBuilder(); 
	 if(isFrameworkClass(o.getClass())){ 
	    s.append(o); 
		return s.toString(); 
	 } 
	 
	 List fields = new ArrayList(); 
	 fields = getAllFieldsRec(o.getClass() ,fields ); 
	 s.append("Object{"); 
	 for (Field f : fields) { 
	    s.append(f.getName()); 
		s.append("=("); 
		try { 
		   f.setAccessible(true); 
		   if (!ignoreType) { 
		      String type = f.getGenericType().toString(); 
			  s.append(type); s.append(", "); 
		   } 
		   Object value = f.get(o); 
		   describeInner(value, s); 
		   s.append(")"); 
		} catch (IllegalArgumentException | IllegalAccessException e) { 
		   e.printStackTrace(); 
		} 
		s.append(", "); 
     } 
	 
	 s.replace(s.length() - 2, s.length(), ""); 
	 s.append("} "); 
	 return s.toString();  
	 
  } 
  
  public String describe(Object o) { 
     if (o != null) { 
	    StringBuilder sb = new StringBuilder(); 
		describeInner(o, sb); 
		return sb.toString(); 
	 } 
	 return null;  
  } 
}
  Stay connected and stay Subscribed