Back to Blog
Java

Java Object toString: Overriding Default Behavior

java object tostring: Learn how Java's Object.toString() works by default, how to override it for readable output, and how records, null handling, and performance shap...

JavatoStringObject classJava recordsString formattingDebugging
Diagram showing a Java object being converted into a readable string representation with field names and values.

The java object tostring behavior starts with the default implementation on Object. When you log an object, print it, or include it in an exception message, Java calls toString() on that object. The base implementation returns the class name, an @ symbol, and the hash code in hexadecimal, which is rarely useful for diagnosing problems. Overriding toString() is the standard way to make objects produce readable, informative output.

What Object.toString() Produces by Default

Every Java class inherits toString() from Object unless it overrides the method. For an instance of com.example.User, the default output looks like:

com.example.User@7a81197d

The value after @ is the object's hash code rendered in unsigned hexadecimal. This value is not stable across runs, and it reveals nothing about the object's fields. If you log a list of such objects, you get a sequence of class names and hash codes that is hard to work with. The default exists mainly as a fallback; it is not designed for human-readable diagnostics.

Overriding toString() in a Simple Class

The direct fix is to override toString() and return a string built from the object's fields. A minimal implementation using string concatenation:

public class User { private final String name; private final int age; public User(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } }

The @Override annotation matters here. If you accidentally declare the method with a different signature, such as public String toString(String prefix), the annotation causes a compile error. Without it, the compiler silently treats the method as an overload, and the object continues to use the default toString() in logs.

Choosing Between Concatenation and StringBuilder

For a class with a few fields, string concatenation is readable, and the compiler rewrites it into StringBuilder operations. When the number of fields grows, the chain of + operators becomes hard to scan. A StringBuilder keeps the structure explicit:

@Override public String toString() { return new StringBuilder() .append("Order{id=").append(id) .append(", status=").append(status) .append(", total=").append(total) .append('}') .toString(); }

String.format() is another option, but it parses the format string and boxes primitive arguments, which adds overhead. Since toString() can be called frequently in logging and debugging paths, String.format() is usually the weakest choice when the method is on a hot path.

Handling Null Fields Safely

Concatenating a null reference produces the literal string "null", which is often acceptable. The problem appears when a field is an object and you want to call toString() on it explicitly. A null field then throws a NullPointerException. The Objects.toString() helper handles this without extra conditionals:

import java.util.Objects; @Override public String toString() { return "Address{street=" + Objects.toString(street, "unknown") + ", city=" + Objects.toString(city, "unknown") + "}"; }

Objects.toString(value, defaultValue) returns the default when the value is null and otherwise returns value.toString(). This keeps the output predictable when optional fields are missing.

Records and Auto-Generated toString()

Java records, standard since Java 16, generate toString() automatically from their components:

public record Product(String sku, String name, BigDecimal price) { }

Calling toString() on a Product instance produces output like:

Product[sku=SKU-123, name=Wireless Mouse, price=29.99]

The generated implementation stays in sync with the record's components, so it cannot drift out of date as fields are added or removed. If the default format does not suit your logging or display needs, you can override toString() inside the record body just as you would in a regular class.

Performance and Maintainability Concerns

toString() runs automatically in log statements, exception messages, and debugger output. If the method builds a large string from a deeply nested object graph, it can add noticeable work to logging paths. Keep the method cheap: include scalar fields and short summaries of related objects rather than recursively rendering entire object graphs.

Recursive toString() calls are a real failure mode. If two objects reference each other and each includes the other in its toString() output, the call stack grows until a StackOverflowError is thrown. Bidirectional parent-child relationships are the typical cause. Include only an identifier or a brief summary of the related object to break the cycle.

The output format can also become a contract. If log-parsing tools or tests assert on the exact string, changing field names or the format breaks those consumers. When that is the case, treat the format as part of the class's public behavior and update the affected tests and parsers together.

Common Mistakes and Edge Cases

Arrays do not override toString(). Calling toString() on an array directly produces output like [Ljava.lang.String;@4c873330. Use Arrays.toString() for one-dimensional arrays and Arrays.deepToString() for nested arrays.

Sensitive fields deserve special attention. If an object holds a password, token, or other secret, the default toString() may expose it in logs. Override the method to omit or mask such fields. This is a practical concern in production systems where logs are collected and searched by other teams.

Another recurring mistake is forgetting the @Override annotation and creating an overload instead of an override. The annotation turns that silent bug into a compile error, which is why it should be present in every toString() implementation.

java object tostring: Practical Usage and Code Examples | RYUSLOG DEV