Tuesday, December 3, 2024

Building a Better toString() Method

The humble toString() method is widely implemented across numerous programming languages. It provides a simple and convenient mechanism for representing an object’s state in readable form. Not surprisingly, it’s used extensively in debugging during development. Its other many uses include logging, passing informative error messages to exceptions, and displaying information to the client without any need for specialized formatting and conversion methods. Unfortunately, this powerful tool in debugging and troubleshooting is all-too-often left to its default implementation by developers. And that’s a shame, because it’s really easy to write your own. In today’s article, I’ll show you how to write a dynamic toString() in Java.

Java’s Default toString() Implementation

The makers of Java have made to String() accessible to all objects by placing it in the root Object from which all other objects inherit from. All that’s required in order to use it is to either invoke it explicitly or pass the object to a method that expects a String argument, like sysout. That causes an object’s toString() to be invoked automagically. Unfortunately, the default Object implementation of toString() is not the most informative. It returns a string consisting of the name of the class, the “at” sign character ‘@’, followed by the hexadecimal representation of the hash code of the object. In other words, this method returns a string equal to the value of:

getClass().getName() + '@' + Integer.toHexString(hashCode())

Here’s what was displayed for an instance of the Bank class that we’ll be using as our sample object today:

ca.gc.cbsa.banking.models.Bank@fc5408

At least we know that it’s a Bank class!

Generally, developers will want to override toString() to display more pertinent details about an object state – i.e. its attributes. That usually leads to something like this:

@Override
public String toString(){
  return  "Account Number: "        + accountNumber + "n" + 
          "Account Owner: "         + accountOwner + "n" +
          "Overdraft Protection?: " + hasOverdraftProtection + "n" +
          "Account Balance: "       + balance + "n" +
          "Service Fee: "           + serviceFee;
}

Sure, it produces far superior output to the default implementation, but who want’s to repeat that exercise for every class they create?

A Simple toString() Implementation using Reflection

Java’s powerful Reflection API allows us to inspect an application’s classes, interfaces, fields and methods at runtime, without knowing their names at compile time. Because Reflection can be utilized to circumvent Object-Oriented programming’s built-in security features – namely encapsulation – reliance on Reflection should be mainly limited to development, testing, and debugging. Of course, there are exceptions, such as mapping objects to tables in a database at runtime, like Butterfly Persistence does.

But before we do that, let’s take a look at our test class. The Bank class has three Fields: the branch name, an ArrayList of accounts, and a HashMap that links accounts to their owners. Without a scope modifier, these Fields would have the default visibility of Package. Note that methods have mostly been removed for brevity.

package ca.gc.cbsa.banking.models;

import java.util.ArrayList;
import java.util.HashMap;

import com.robgravelle.utils.ObjectUtils;

public class Bank {
	String branchName;
	ArrayList<Account> accounts = new ArrayList<Account>();
	HashMap<String, ArrayList<Account>> clientMap = new HashMap<String, ArrayList<Account>>();
	
	public Bank(String aBranchName, ArrayList<Account> someAccounts){
		branchName = aBranchName;
		accounts = someAccounts;
		initializeClientMap();
	}
	
	public ArrayList<Account> getAccounts() {
		return accounts;
	}
	
	public void setAccounts(ArrayList<Account> accounts) {
		this.accounts = accounts;
	}
	public String getBranchName() {
		return branchName;
	}
	public void setBranchName(String branchName) {
		this.branchName = branchName;
	}
}

This simple toString() method iterates over a Class’s declared fields and displays them in a nicely formatted way. Reflection methods are exposed via the Class object. It’s a special type of object that provides information about the class. Obtaining a reference to the Class is as easy as calling a class instance’s getClass() method. From there, the getDeclaredFields() method returns an array of declared fields. The Field object in turn has a method to get the field name and value: getName() and get() respectively.

@Override 
public String toString() {
  StringBuilder result = new StringBuilder();
  String newLine = System.getProperty("line.separator");

  result.append(this.getClass().getName());
  result.append(" Object {");
  result.append(newLine);

  //determine fields declared in this class only (no fields of superclass)
  Field[] fields = this.getClass().getDeclaredFields();

  //print field names paired with their values
  for (Field field : fields) {
    result.append("  ");
    field.setAccessible(true);
    try {
      result.append(field.getName());
      result.append(": ");
      //requires access to private field:
      result.append(field.get(this));
    }
    catch (IllegalAccessException ex) {
      System.out.println(ex);
    }
    result.append(newLine + newLine);
  }
  result.append("}");

  return result.toString();
}

By calling Field.setAcessible(true) you turn off the access checks for the Field instance so that you can access it even if it is private, protected or package scope. Setting it to true doesn’t hurt if the Field has public scope, so there’s really no harm in applying it to every field.

Testing Our toString() Method

Now we are ready to try out our toString() method. The Bank constructor requires a branch name and an ArrayList of BankAccounts (not shown). Each account in turn contains an account number, the holder’s name, and opening balance.

public static void main(String[] args) {
  ArrayList<Account> accounts = new ArrayList<Account>() {{
  	this.add(new BankAccount(1234, "Rob Gravelle", 100.00));
  	this.add(new BankAccount(2345, "Al Bundy", 0.00));
  	this.add(new BankAccount(3456, "Sue Bastianich", 1000.00));
  }};
  Bank bank = new Bank("Main branch", accounts );
  System.out.println(bank);
}

Running the above main() method produces the following informative output:

ca.gc.cbsa.banking.models.Bank Object {
  branchName: Main branch
  
  accounts: [Account Number: 1234
Account Holder: Rob Gravelle
Account Balance: 100.0
Service Fee: 5.0, Account Number: 2345
Account Holder: Al Bundy
Account Balance: 0.0
Service Fee: 5.0, Account Number: 3456
Account Holder: Sue Bastianich
Account Balance: 1000.0
Service Fee: 0.0]

  clientMap: {Sue Bastianich=[Account Number: 3456
Account Holder: Sue Bastianich
Account Balance: 1000.0
Service Fee: 0.0], Rob Gravelle=[Account Number: 1234
Account Holder: Rob Gravelle
Account Balance: 100.0
Service Fee: 5.0], Al Bundy=[Account Number: 2345
Account Holder: Al Bundy
Account Balance: 0.0
Service Fee: 5.0]}
}

Promoting Reusability

For better reusability, you can place the real code in a utility class:

package com.robgravelle.utils;

import java.lang.reflect.Field;
import java.util.ArrayList;

import ca.gc.cbsa.banking.interfaces.Account;
import ca.gc.cbsa.banking.models.Bank;
import ca.gc.cbsa.banking.models.BankAccount;

public class ObjectUtils {
	public static String objToString(Object obj) {
		  StringBuilder result = new StringBuilder();
		  String newLine = System.getProperty("line.separator");

		  result.append(obj.getClass().getName());
		  result.append(" Object {");
		  result.append(newLine);

		  //determine fields declared in this class only (no fields of superclass)
		  Field[] fields = obj.getClass().getDeclaredFields();
          
		  //print field names paired with their values
		  for (Field field : fields) {
			field.setAccessible(true);
		
		    result.append("  ");
		    try {
		      result.append(field.getName());
		      result.append(": ");
		      //requires access to private field:
		      result.append(field.get(obj));
		    }
		    catch (IllegalAccessException ex) {
		      System.out.println(ex);
		    }
		    result.append(newLine + newLine);
		  }
		  result.append("}");

		  return result.toString();
	}
}

Now all that the toString() does is call the static delegate method and pass the class instance to it:

@Override
public String toString() {
  return ObjectUtils.objToString(this);
}

Conclusion

Even though it’s not hard to do, I wouldn’t recommend writing your own generic toString() method from scratch unless you have very particular requirements. In up-coming articles, we’ll learn how to piggy-back on the shoulders of toString() trail blazers!

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured