The Big Three in Java, Part One: toString()

Introduction

When designing a new class in Java, there are three methods inherited from the Object class that we should typically override. I refer to these methods as the “big three”: the Object.toString(), Object.equals(Object), and Object.hashCode() methods. We’ll explore, in a series of four blog posts, why and how you should override these methods.

Today, in the first blog post in the series, we’ll start by exploring the Object.toString() method.

The second blog post in this series, about Object.equals(Object), is now available. The third blog post, about Object.hashCode(), is also available. The final blog post, about some additional subtleties to watch out for with Object.equals(Object), is forthcoming.

A String Representation of an Object

The purpose of the Object.toString() method is to provide a String representation of an object and its state. Because all classes in Java extend the Object class, we can override the Object.toString() method in any class that we implement. By doing so, we can customize how we want an object of that class to be represented as a String.

Printing Objects to the Console

The first time that most students encounter the Object.toString() method is when we want to print a String representation of an object to the console.

Consider, for example, if we have Cow class, where Cows have member variables representing the Cow‘s name and age. We could construct a Cow object like this:

Cow molly = new Cow("Molly", 3);

Ideally, if we were to print a representation of a Cow to the console, we’d output some useful information about the Cow. Consider if we were to write the following:

System.out.println(molly);

It would be nice if the output to our console were something to the effect of:

Cow - Molly, age 3

However, that isn’t what gets output by default. Let’s consider a complete implementation of the Cow class with those two member variables, and a Cow.main(String[]) method that outputs a String representation of our Cow to the console:

public class Cow
{
    private String name;
    private int age;

    public Cow(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public static void main(String[] args)
    {
        Cow molly = new Cow("Molly", 3);
        System.out.println(molly);
    }
}

What gets output to our console isn’t anything (particularly) useful, like the Cow‘s name or age. Instead, the output of this program looks something like the following:

Cow@4c98385c

The hexadecimal digits after Cow@ may be different when you run this program, but in any case, the output isn’t terribly meaningful for us. A future blog post in this series, about the Object.hashCode() method, will discuss what those hexadecimal digits represent. But, for now, let’s get to the more important topic at hand: how can we make the output of a Cow to our console useful?

A First Draft of the Cow.toString() Method

Here’s where the Cow.toString() method comes into play. That method has to return a String. Whenever we try to print a Cow object to the console, it’s that String that’ll be output (in place of the "Cow@…" String).

We can build a meaningful String that represents a Cow using code that looks like the following:

String strRep = "Cow - " + this.name + ", age " + this.age;

For our Cow named Molly, that concatenation operation would create the following String:

"Cow - Molly, age 3"

If we return that String from the Cow.toString() method, that’s what’ll be output to the console when we call System.out.println(molly).

As a first draft, our Cow.toString() could look like this:

public class Cow
{
    [...]

    public String toString()
    {
        String strRep = "Cow - " + this.name +
            ", age " + this.age;
        return strRep;
    }
}

Now, when we run our example Cow.main(String[]) method,

public static void main(String[] args)
{
    Cow molly = new Cow("Molly", 3);
    System.out.println(molly);
}

we get our desired output:

Cow - Molly, age 3

Improving the Cow.toString() Method

As mentioned, the code above was just a first draft of the Cow.toString() method. There are two improvements that we can make to it.

First, because the Cow.toString() method overrides the Object.toString() method, we should include the @Override annotation on the Cow class’ toString() method.

The @Override annotation tells the Java compiler that the Cow.toString() method is meant to override a parent class’ method of the same name. Adding this annotation doesn’t change the functionality or correctness of the Cow.toString() method we previously wrote. But, it does trigger a compiler error if we make a typo in the name of the Cow.toString() method.

For example, because of the @Override annotation, the following code will produce a compiler error. Notice, in particular, the incorrect capitalization of the method name:

public class Cow
{
    [...]

    @Override
    public String tostring()
    {
        String strRep = "Cow - " + this.name +
            ", age " + this.age;
        return strRep;
    }
}

Attempting to compile this code results in the following error:

error: method does not override or implement a method from a supertype

Without the @Override annotation, the Java compiler will happily produce a Cow class with a method called Cow.tostring(). That method will never be invoked, unless we explicitly call molly.tostring(). That’s because a method named Cow.tostring() is different from a method named Cow.toString().

The other change we can make to the first draft of our Cow.toString() method is to eliminate the variable strRep. Because we never make use of that variable — aside from returning it — we can skip a step and return the result of the String concatenation directly.

After making these two changes, our improved Cow.toString() method — with a single-line body — now looks like this:

@Override
public String toString()
{
    return "Cow - " + this.name +
        ", age " + this.age;
}

Let’s place that improved method into our complete program:

public class Cow
{
    private String name;
    private int age;

    public Cow(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString()
    {
        return "Cow - " + this.name +
            ", age " + this.age;
    }

    public static void main(String[] args)
    {
        Cow molly = new Cow("Molly", 3);
        System.out.println(molly);
    }
}

I recommend that students try compiling and running this improved program, to see that it still produces our desired output:

Cow - Molly, age 3

Behind the Scenes With String.valueOf(Object)

The final two sections of this blog post briefly explore what’s happening behind the scenes in Java when we try to get a String representation of an Object. While most students should feel free to skip over these technicalities, I’ve included them here for students who are interested in the minute details.

Up until now, we’ve discussed how the Cow.toString() method gets called when we output a Cow to the console. To be more precise, though, it’s actually the String.valueOf(Object) method that gets called. The implementation of that method looks something like the following:

public class String
{
    [...]

    public static String valueOf(Object obj)
    {
        if (obj == null) {
            return "null";
        }
        else {
            return obj.toString();
        }
    }
}

The String.valueOf(Object) method deals with the possibility that we’re trying to output a null reference to the console. That is, the following code,

public static void main(String[] args)
{
    Cow c = null;
    System.out.println(c);
}

won’t produce a NullPointerException. Instead, because String.valueOf(null) returns the String "null", the program outputs:

null

That said, for the purposes of learning about the toString() method, this is a minor detail. For any situation where an object is non-null, it’s the class’ toString() method that produces its String representation.

Other Automatic Invocations

Attempting to print an object to the console isn’t the only time that the String.valueOf(Object) method — and hence an object’s toString() method — gets called.

That method also gets called when we concatenate any object onto a String. As an example, let’s consider the following code:

public static void main(String[] args)
{
    Cow molly = new Cow("Molly", 3);
    String str = "In the pasture, there is: " + molly;
    System.out.println(str);
}

The concatenation operation (that is, the + operation) automatically invokes the String.valueOf(Object) method. It’ll produce a String representation of the Cow named Molly, then concatenate that String onto the String about the pasture. So, this program will output:

In the pasture, there is: Cow - Molly, age 3

Because String.valueOf(Object) works even with a null argument, the following code,

public static void main(String[] args)
{
    Cow c = null;
    String str = "In the pasture, there is: " + c;
    System.out.println(str);
}

will produce the output:

In the pasture, there is: null

There are also some other times that String.valueOf(Object) gets invoked automatically, such as when we use the StringBuilder.append(Object) method, or the String.format(String, Object...) method. The use of those methods is beyond the scope of this blog post. But that being said, it’s worth keeping in mind that String.valueOf(Object), and thus the toString() method of any class we implement, can get called in numerous circumstances.

Conclusion

When we write a class in Java, we should typically override the “big three” methods: Object.toString(), Object.equals(Object), and Object.hashCode(). In this first blog post about the “big three”, we explored the Object.toString() method.

By overriding the Object.toString() method in any class we write, we can make System.out.println(Object) output meaningful information about objects of our class. While not always the case, we can often implement our own class’ toString() method with just a single line of code in its method body.

Keep in mind that, whenever we override a method such as Object.toString(), we should always include the @Override annotation on our overriding implementation. This annotation helps catch typos in our code, such as if we accidentally name the method in our class tostring() instead of toString().

For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.