toString() Method
Every Java object has a toString() method. It returns a human-readable String representation of the object, and you will encounter it constantly — in log messages, debugger output, and string concatenation. Understanding how it works, and how to override it properly, is one of the most practical skills you can develop as a Java developer.
Where Does toString() Come From?
toString() is defined in java.lang.Object, the root superclass of every class in Java. Because all classes implicitly extend Object (see Object Class), every object you ever create already has a toString() method before you write a single line.
The default implementation looks roughly like this:
// Inside java.lang.Object (simplified)
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
This produces output such as com.example.Person@7ef88735 — the fully qualified class name, an @ separator, and the object’s hash code in hexadecimal. That is rarely useful on its own, which is why overriding toString() is considered a best practice for any meaningful class.
The Default Output Problem
public class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person("Alice", 30);
System.out.println(p); // calls p.toString() automatically
}
}
Output:
Person@6d06d69c
That hexadecimal address tells you nothing about the actual data. Let’s fix that.
Overriding toString()
Override toString() in your class by annotating it with @Override and returning a descriptive String:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person("Alice", 30);
System.out.println(p);
}
}
Output:
Person{name='Alice', age=30}
Much better! Now any time you print a Person, log it, or concatenate it into a message, you see real data.
Tip: Always use
@Override. It tells the compiler you intend to override a parent method, so you get a compile-time error instead of a silent bug if you accidentally misspell the method name.
When toString() Is Called Automatically
Java calls toString() implicitly in several common situations:
- String concatenation —
"User: " + personcallsperson.toString()behind the scenes. System.out.println(obj)—PrintStream.printlncallsString.valueOf(obj), which callsobj.toString().- String formatting —
String.format("Hello %s", obj)with%scallsobj.toString(). StringBuilder.append(obj)— appends the result ofobj.toString().
Person p = new Person("Bob", 25);
// All four call toString() automatically:
System.out.println(p);
System.out.println("Person: " + p);
System.out.printf("Name: %s%n", p);
StringBuilder sb = new StringBuilder();
sb.append(p);
System.out.println(sb);
Output:
Person{name='Bob', age=25}
Person: Person{name='Bob', age=25}
Name: Person{name='Bob', age=25}
Person{name='Bob', age=25}
Using String.valueOf() vs toString()
String.valueOf(obj) is the null-safe way to get a string representation:
Person p = null;
// This throws NullPointerException:
// System.out.println(p.toString());
// This safely prints "null":
System.out.println(String.valueOf(p));
System.out.println("" + p); // also safe — concatenation handles null
Output:
null
null
Warning: Calling
.toString()directly on anullreference throws aNullPointerException. PreferString.valueOf()orObjects.toString(obj, "default")when the reference might be null.
toString() with Arrays
A common beginner trap: arrays do NOT override toString(), so printing them gives the unhelpful default.
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers); // unhelpful!
System.out.println(Arrays.toString(numbers)); // useful!
String[] names = {"Alice", "Bob"};
System.out.println(Arrays.toString(names));
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(matrix));
Output:
[I@6d06d69c
[1, 2, 3, 4, 5]
[Alice, Bob]
[[1, 2], [3, 4]]
See Arrays Utility Class for more on Arrays.toString() and Arrays.deepToString().
toString() in Inheritance
When you override toString() in a subclass, the override applies polymorphically — whatever the runtime type of the object is, that class’s toString() gets called.
class Animal {
String name;
Animal(String name) { this.name = name; }
@Override
public String toString() {
return "Animal{name='" + name + "'}";
}
}
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name);
this.breed = breed;
}
@Override
public String toString() {
return "Dog{name='" + name + "', breed='" + breed + "'}";
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog("Rex", "Labrador"); // upcasted reference
System.out.println(a); // Dog's toString() runs
}
}
Output:
Dog{name='Rex', breed='Labrador'}
This is runtime polymorphism in action — the JVM dispatches to the actual runtime type’s method, not the declared type.
You can also call the parent’s toString() from within a subclass override using super:
@Override
public String toString() {
return super.toString() + ", breed='" + breed + "'";
}
Practical Patterns
Using StringBuilder for complex objects
For objects with many fields, building the string with StringBuilder is more efficient than repeated + concatenation:
@Override
public String toString() {
return new StringBuilder("Person{")
.append("name='").append(name).append('\'')
.append(", age=").append(age)
.append(", email='").append(email).append('\'')
.append('}')
.toString();
}
Records generate toString() automatically
Java 16+ Records automatically generate a sensible toString() (along with equals() and hashCode()), so you do not have to write it yourself:
record Point(int x, int y) {} // toString() auto-generated
public class Main {
public static void main(String[] args) {
Point p = new Point(3, 7);
System.out.println(p);
}
}
Output:
Point[x=3, y=7]
Tip: If you are using Java 16+ and your class is just a data carrier, consider using a
record— you gettoString(),equals(), andhashCode()for free.
Under the Hood
When you write "Hello, " + obj, the Java compiler transforms it into a StringBuilder-based expression (or in newer JVM versions, uses invokedynamic with StringConcatFactory). Either way, obj.toString() is invoked to get the string value.
toString() is a virtual method — it lives in the vtable of every class. When the JVM dispatches the call, it looks up the most specific override at runtime using the vtable dispatch mechanism. This is why you always get the most-derived class’s version, even through a supertype reference.
The @Override annotation has zero runtime cost — it is erased by the compiler and exists only to enable a compile-time check. Use it freely.
For logging libraries (like SLF4J or Log4j2), many use lazy evaluation — they defer calling toString() until the message actually needs to be rendered. This means a well-implemented toString() should be side-effect-free and reasonably fast (avoid database calls, network I/O, or acquiring locks inside toString()).
toString() Contract and Best Practices
There is no enforced contract for toString() (unlike equals() and hashCode()), but the community has converged on these guidelines:
| Guideline | Why |
|---|---|
| Include all significant fields | Makes debugging and logging useful |
| Keep it side-effect-free | May be called unexpectedly by debuggers or frameworks |
| Keep it fast | Avoid I/O, locks, or expensive computation |
| Do not parse it programmatically | Output format can change; use getters for logic |
Use @Override | Catches typos at compile time |
| Handle null fields safely | Prevents NPE inside your own toString() |
@Override
public String toString() {
// Safely handle a potentially-null field
return "Order{id=" + id + ", customer=" + (customer != null ? customer.getName() : "none") + "}";
}
Related Topics
- Object Class — where
toString(),equals(), andhashCode()are defined - Strings — the
Stringtype thattoString()always returns - StringBuilder — the efficient way to build strings inside
toString() - Records — Java 16+ data classes that auto-generate
toString() - Runtime Polymorphism — why overriding
toString()dispatches correctly at runtime - Object Cloning — another
Objectmethod you will commonly override alongsidetoString()