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 Cow
s 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.