Object-Oriented Programming Basics

The toString Method

15 min Lesson 9 of 14

The toString Method

Every object in Java has a built-in ability to represent itself as text. That ability comes from the toString() method, which is defined in java.lang.Object — the root of the entire Java class hierarchy. Because every class you write automatically inherits from Object, every object already has a toString() method before you write a single line.

What the Default toString Looks Like

The default implementation, inherited from Object, produces a string that combines the fully-qualified class name with the object's identity hash code in hexadecimal:

// Default format: ClassName@hexHashCode // e.g. BankAccount@7ef88735

Let us see it in practice. Suppose you have a simple BankAccount class and you try to print it:

public class BankAccount { private String owner; private double balance; public BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } } public class Main { public static void main(String[] args) { BankAccount account = new BankAccount("Alice", 1500.00); System.out.println(account); // BankAccount@6d06d69c } }

The output is meaningless for debugging. You cannot tell the owner or the balance just by looking at that hash code. This is exactly why overriding toString() is one of the first things an experienced Java developer does when creating a new class.

When is toString called automatically? Java calls toString() on an object automatically whenever it needs a String representation: inside System.out.println(obj), during string concatenation ("Account: " + obj), and inside debuggers, loggers, and most IDE inspectors. You rarely call it by name; Java does it for you.

Overriding toString

Overriding means providing your own version of the method that replaces the inherited one. The signature must match exactly: it must be public, return a String, and take no parameters. The @Override annotation is not required, but it is strongly recommended — it tells the compiler you intend to override an inherited method, so it will catch typos or wrong signatures for you.

public class BankAccount { private String owner; private double balance; public BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } @Override public String toString() { return "BankAccount{owner='" + owner + "', balance=" + balance + "}"; } } public class Main { public static void main(String[] args) { BankAccount account = new BankAccount("Alice", 1500.00); System.out.println(account); // Output: BankAccount{owner='Alice', balance=1500.0} } }

Now the output is immediately useful. Anyone reading a log or debugging in an IDE can understand the object at a glance.

Formatting Numbers in toString

Raw double values can produce ugly output like 1500.0000000001 due to floating-point precision. Use String.format() to control the presentation:

@Override public String toString() { return String.format("BankAccount{owner='%s', balance=%.2f}", owner, balance); } // Output: BankAccount{owner='Alice', balance=1500.00}

%.2f formats a floating-point number to exactly two decimal places — perfect for currency.

Use String.format() for anything beyond simple concatenation. It is more readable when you have several fields, and it gives you full control over number formats, padding, and date patterns. Think of it as a template where %s is a string slot and %d is an integer slot.

A Realistic Multi-Field Example

Here is a Product class with several fields to show how toString() scales:

public class Product { private String name; private String category; private double price; private int stockQuantity; public Product(String name, String category, double price, int stockQuantity) { this.name = name; this.category = category; this.price = price; this.stockQuantity = stockQuantity; } @Override public String toString() { return String.format( "Product{name='%s', category='%s', price=%.2f, stock=%d}", name, category, price, stockQuantity ); } } public class Main { public static void main(String[] args) { Product p = new Product("Wireless Keyboard", "Electronics", 49.99, 200); System.out.println(p); // Product{name='Wireless Keyboard', category='Electronics', price=49.99, stock=200} } }

toString and String Concatenation

Because Java calls toString() automatically during concatenation, you can embed objects directly in larger strings:

BankAccount account = new BankAccount("Alice", 1500.00); String message = "Processing payment for: " + account; System.out.println(message); // Processing payment for: BankAccount{owner='Alice', balance=1500.00}
Do not expose sensitive data in toString. A toString() that includes passwords, credit card numbers, or secret keys is a security risk — those strings end up in logs, stack traces, and error messages that may be visible to unintended audiences. Include only fields that are safe to display publicly.

Common toString Styles

There is no single required format, but the Java community tends to use a few consistent patterns:

  • Curly-brace styleClassName{field1=value1, field2=value2} (used in the examples above, matches what IDEs generate by default)
  • Bracket styleClassName[field1=value1, field2=value2]
  • Prose style — a natural-language sentence, useful when the object is shown to end users rather than developers

Choose the style that fits your audience. Use the curly-brace or bracket style for developer-facing output; use a natural sentence when the string appears in a UI or email.

Summary

The toString() method turns any object into a human-readable string. The default inherited version from Object produces a hex hash that is useless for debugging. By overriding it with @Override public String toString() you gain meaningful output in println, string concatenation, logs, and debuggers — all for free. Keep sensitive fields out, use String.format() for clean number formatting, and be consistent with your chosen style across the codebase.