Java's Object Methods: toString()

Introduction

In this article I will be kicking off a series of articles describing the often forgotten about methods of the Java language's base Object class. Below are the methods of the base Java Object, which are present in all Java objects due to the implicit inheritance of Object. Links to each article of this series are included for each method as the articles are published.

  • toString (you are here)
  • getClass
  • equals
  • hashCode
  • clone
  • finalize
  • wait & notify

In the sections that follow I will be describing what these methods are, their base implementations, and how to override them when needed. The focus of this first article is the toString() method which is used to give a string representation that identifies an object instance and convey its content and / or meaning in human readable form.

The toString() Method

At first glance the toString() method may seem like a fairly useless method and, to be honest, its default implementation is not very helpful. By default the toString() method will return a string that lists the name of the class followed by an @ sign and then a hexadecimal representation of the memory location the instantiated object has been assigned to.

To help aid in my discussion of the ubiquitous Java Object methods I will work with a simple Person class, defined like so:

package com.adammcquistan.object;

import java.time.LocalDate;

public class Person {  
    private String firstName;
    private String lastName;
    private LocalDate dob;

    public Person() {}

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public LocalDate getDob() {
        return dob;
    }

    public void setDob(LocalDate dob) {
        this.dob = dob;
    }
}

Along with this class I have a rudimentary Main class to run the examples shown below to introduce the features of the base implementation of toString().

package com.adammcquistan.object;

import java.time.LocalDate;

public class Main {  
    public static void main(String[] args) {
        Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person me2 = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person you = new Person("Jane", "Doe", LocalDate.parse("2000-12-25"));
        System.out.println("1. " + me.toString());
        System.out.println("2. " + me);
        System.out.println("3. " + me + ", " + you);
        System.out.println("4. " + me + ", " + me2);
}

The output looks like this:

1. [email protected]  
2. [email protected]  
3. [email protected], [email protected]  
4. [email protected], [email protected]  

The first thing to mention is that the output for lines one and two are identical, which shows that when you pass an object instance to methods like print, println, printf, as well as loggers, the toString() method is implicitly called.
Additionally, this implicit call to toString() also occurs during concatenation as shown in line 3's output.

Ok, now its time for me to interject my own personal opinion when it comes to Java programming best practices. What stands out to you as potentially worrisome about line 4 (actually any of the output for that matter)?

Hopefully you are answering with a question along these lines, "well Adam, its nice that the output tells me the class name, but what the heck am I to do with that gobbly-gook memory address?".

And I would respond with, "Nothing!". Its 99.99% useless to us as programmers. A much better idea would be for us to override this default implementation and provide something that is actually meaningful, like this:

public class Person {  
    // omitting everyting else remaining the same

    @Override
    public String toString() {
        return "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
    }
}

Now if I rerun the earlier Main class I get the following greatly improved output:

1. <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23>  
2. <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23>  
3. <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23>, <User: firstName=Jane, lastName=Doe, dob=2000-12-25>  
4. <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23>, <User: firstName=Adam, lastName=McQuistan, dob=1987-09-23>  

OMG! Something that I can read! With this implementation I now stand a fighting chance of actually being able to comprehend what is going on in a log file. This is especially helpful when tech support is screaming about erratic behavior relating to People instances in the program I'm on the hook for.

Caveats for Implementing and Using toString()

As shown in the previous section, implementing an informative toString() method in your classes is a rather good idea as it provides a way to meaningfully convey the content and identity of an object. However, there are times when you will want to take a slightly different approach to implementing them.

For example, say you have an object that simply contains too much state to pack into the output of a toString() method or when the object mostly contains a collection of utilities methods. In these cases it is often advisable to output a simple description of the class and its intentions.

Consider the following senseless utility class which finds and returns the oldest person of a list of People objects.

public class OldestPersonFinder {  
    public List<Person> family;

    public OldestPersonFinder(List<Person> family) {
        this.family = family;
    }

    public Person oldest() {
        if (family.isEmpty()) {
            return null;
        }
        Person currentOldest = null;
        for (Person p : family) {
            if (currentOldest == null || p.getDob().isAfter(currentOldest.getDob())) {
                currentOldest = p;
            }
        }
        return currentOldest;
    }

    @Override
    public String toString() {
        return "Class that finds the oldest Person in a List";
    }
}

In this case it would not be very helpful to loop over the entire collection of Person objects in the family List instance member and build some ridiculously large string to return representing each Person. Instead, it is much more meaningful to return a string describing the intentions of the class which in this case is to find the Person who is the oldest.

Another thing that I would like to strongly suggest is to make sure you provide access to all information specific to your class's data that you include in the output to your toString() method.

Say, for example, I had not provided a getter method for my Person class's dob member in a vain attempt to keep the person's age a secret. Unfortunately, the users of my Person class are eventually going to realize that they can simply parse the output of the toString() method and acquire the data they seek that way. Now if I ever change the implementation of toString() I am almost certain to break their code. On the other side let me say that its generally a bad idea to go parsing an object's toString() output for this very reason.

Conclusion

This article described the uses and value in the often forgotten about toString() method of the Java base Object class. I have explained the default behavior and given my opinion as to why I think it is a best practice to implement your own class specific behavior.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Author image
Lincoln, Nebraska Twitter