Java Tutorial
🔍

Java toString() Method

Java toString() Method

Every object in Java has a toString() method. You never call it explicitly on most objects — Java calls it for you every time you pass an object to System.out.println, concatenate it with a string, or log it. When you have not overridden it, you see something like com.example.Order@7852e922 in your logs. When you have, you see Order{id='ORD-001', amount=Rs.1499.0, status='PLACED'}.

That difference determines how fast you can debug a production issue. A log file full of memory addresses tells you nothing. A log file with readable object state tells you exactly what was happening when something went wrong.

What Is toString()?

toString() is a method declared in java.lang.Object — the root of Java's class hierarchy. Every class inherits it. Its job is to return a String that represents the object in a human-readable form.

The default implementation in Object is:

// Object.java (simplified — this is what the JDK does internally) public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

This produces output like com.example.model.Order@1b6d3586. The class name is useful. The hex hash code identifies the object instance but carries no domain meaning. For anything beyond debugging object identity, the default is useless.

When Java Calls toString() Implicitly

Java invokes toString() automatically in four situations. Understanding each one explains why overriding it has such broad impact.

// File: ImplicitToStringDemo.java public class ImplicitToStringDemo { static class Product { private final String sku; private final double price; Product(String sku, double price) { this.sku = sku; this.price = price; } @Override public String toString() { return "Product{sku='" + sku + "', price=Rs." + price + "}"; } } public static void main(String[] args) { Product p = new Product("SKU-201", 1299.0); // 1 — System.out.println calls toString() implicitly System.out.println(p); // 2 — String concatenation calls toString() implicitly String message = "Item added: " + p; System.out.println(message); // 3 — String.valueOf() calls toString() internally String asString = String.valueOf(p); System.out.println(asString); // 4 — StringBuilder.append(Object) calls toString() implicitly StringBuilder sb = new StringBuilder("Cart contains: "); sb.append(p); System.out.println(sb.toString()); } }
Output:
Product{sku='SKU-201', price=Rs.1299.0}
Item added: Product{sku='SKU-201', price=Rs.1299.0}
Product{sku='SKU-201', price=Rs.1299.0}
Cart contains: Product{sku='SKU-201', price=Rs.1299.0}

Every one of these calls reaches the overridden toString() without any explicit .toString() call in the source code. This is why a well-written toString() improves logging, error messages, and debugging across the entire codebase — without changing any calling code.

Overriding toString() — The Right Way

A good toString() override includes the class name and every field that helps identify or understand the object's state. Fields that are sensitive — passwords, tokens, card numbers — must be excluded or masked.

// File: Employee.java import java.util.Objects; public class Employee { private final String employeeId; private final String fullName; private final String department; private String role; private double basicSalary; private final String email; // personal info — include partially private String accessToken; // sensitive — never include public Employee(String employeeId, String fullName, String department, String role, double basicSalary, String email) { this.employeeId = employeeId; this.fullName = fullName; this.department = department; this.role = role; this.basicSalary = basicSalary; this.email = email; } public void setAccessToken(String token) { this.accessToken = token; } public void setRole(String role) { this.role = role; } @Override public String toString() { // Include identifying and diagnostic fields // Mask email domain — show enough to identify, not expose fully // Never include accessToken, passwords, or sensitive credentials String maskedEmail = email != null ? email.substring(0, email.indexOf('@')) + "@***" : "null"; return "Employee{" + "id='" + employeeId + '\'' + ", name='" + fullName + '\'' + ", dept='" + department + '\'' + ", role='" + role + '\'' + ", email='" + maskedEmail + '\'' + '}'; // basicSalary excluded — financial data, not needed in general logs // accessToken excluded — security credential, must never appear in logs } }
// File: ToStringOverrideDemo.java public class ToStringOverrideDemo { public static void main(String[] args) { Employee emp = new Employee( "EMP-042", "Priya Mehta", "Engineering", "Senior Developer", 95000.0, "priya.mehta@company.in" ); emp.setAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."); // toString() called implicitly — sensitive data stays out of logs System.out.println("Logged employee: " + emp); // Explicit call — same result System.out.println(emp.toString()); } }
Output:
Logged employee: Employee{id='EMP-042', name='Priya Mehta', dept='Engineering', role='Senior Developer', email='priya.mehta@***'}
Employee{id='EMP-042', name='Priya Mehta', dept='Engineering', role='Senior Developer', email='priya.mehta@***'}

The access token never appears in the output. The salary never appears. The email is partially masked. A developer reading a log file gets enough context to understand which employee record is involved without exposing sensitive data.

toString() With Collections and Nested Objects

Java's collection classes — ArrayList, HashMap, HashSet — all override toString(). They call toString() on each element to build the output. This means your objects appear correctly in collection output only if you have overridden toString().

// File: CollectionToStringDemo.java import java.util.*; public class CollectionToStringDemo { static class OrderItem { private final String productName; private final int quantity; private final double unitPrice; OrderItem(String productName, int quantity, double unitPrice) { this.productName = productName; this.quantity = quantity; this.unitPrice = unitPrice; } public double lineTotal() { return quantity * unitPrice; } @Override public String toString() { return productName + " x" + quantity + " @ Rs." + unitPrice + " = Rs." + lineTotal(); } } static class Order { private final String orderId; private final List<OrderItem> items; private final String customerName; Order(String orderId, String customerName) { this.orderId = orderId; this.customerName = customerName; this.items = new ArrayList<>(); } public void addItem(OrderItem item) { items.add(item); } public double grandTotal() { return items.stream().mapToDouble(OrderItem::lineTotal).sum(); } @Override public String toString() { return "Order{" + "id='" + orderId + '\'' + ", customer='" + customerName + '\'' + ", items=" + items // ArrayList.toString() calls each item's toString() + ", total=Rs." + grandTotal() + '}'; } } public static void main(String[] args) { Order order = new Order("ORD-701", "Rahul Sharma"); order.addItem(new OrderItem("Laptop Stand", 1, 1499.0)); order.addItem(new OrderItem("Mechanical Keyboard", 1, 4999.0)); order.addItem(new OrderItem("USB-C Hub", 2, 899.0)); System.out.println(order); System.out.println(); // Map toString() calls key.toString() and value.toString() Map<String, Order> orderMap = new HashMap<>(); orderMap.put("ORD-701", order); System.out.println("Map: " + orderMap); } }
Output:
Order{id='ORD-701', customer='Rahul Sharma', items=[Laptop Stand x1 @ Rs.1499.0 = Rs.1499.0, Mechanical Keyboard x1 @ Rs.4999.0 = Rs.4999.0, USB-C Hub x2 @ Rs.899.0 = Rs.1798.0], total=Rs.8296.0}

Map: {ORD-701=Order{id='ORD-701', customer='Rahul Sharma', items=[Laptop Stand x1 @ Rs.1499.0 = Rs.1499.0, Mechanical Keyboard x1 @ Rs.4999.0 = Rs.4999.0, USB-C Hub x2 @ Rs.899.0 = Rs.1798.0], total=Rs.8296.0}}

ArrayList.toString() iterates its elements and calls toString() on each — so every OrderItem in the list renders correctly. HashMap.toString() calls toString() on both keys and values. The entire nested structure prints in one readable line without any explicit formatting code.

toString() vs String.valueOf() vs String.format()

Three approaches produce string representations of objects. Each behaves differently when the object is null.

Aspectobj.toString()String.valueOf(obj)String.format("%s", obj)
Null safetyThrows NullPointerExceptionReturns "null" stringReturns "null" string
When to useWhen you know obj is not nullWhen obj might be nullWhen building formatted output
CallsDirectly on objecttoString() internally if not nulltoString() internally if not null
Explicit call neededYes — obj.toString()Yes — String.valueOf(obj)Yes — inside format string
Implicit callIn concatenation "" + objNever implicitNever implicit
PerformanceFastest direct callNegligible overheadSlower — format parsing
Typical useDirect string conversionSafe null-tolerant conversionBuilding multi-field output strings
// File: ToStringVariantsDemo.java public class ToStringVariantsDemo { static class Invoice { private final String invoiceId; Invoice(String id) { this.invoiceId = id; } @Override public String toString() { return "Invoice[" + invoiceId + "]"; } } public static void main(String[] args) { Invoice invoice = new Invoice("INV-001"); Invoice nullInv = null; // obj.toString() — crashes on null System.out.println("Direct call : " + invoice.toString()); try { System.out.println(nullInv.toString()); // throws } catch (NullPointerException e) { System.out.println("Direct on null : NullPointerException"); } // String.valueOf() — null safe, returns "null" string System.out.println("String.valueOf() : " + String.valueOf(invoice)); System.out.println("valueOf on null : " + String.valueOf(nullInv)); // String concatenation — null safe, returns "null" string System.out.println("Concatenation : " + invoice); System.out.println("null concat : " + nullInv); // prints "null" // String.format() — null safe System.out.println("String.format() : " + String.format("Invoice: %s", invoice)); System.out.println("format on null : " + String.format("Invoice: %s", nullInv)); } }
Output:
Direct call      : Invoice[INV-001]
Direct on null   : NullPointerException
String.valueOf() : Invoice[INV-001]
valueOf on null  : null
Concatenation    : Invoice[INV-001]
null concat      : null
String.format()  : Invoice: Invoice[INV-001]
format on null   : Invoice: null

String concatenation and String.valueOf() both handle null safely by converting it to the string "null". Calling .toString() directly on a potentially null reference is a NullPointerException waiting to happen. In logging and debug code where objects might be null, prefer String.valueOf(obj) or Objects.toString(obj, "unknown").

toString() in Records — Java 16+

Java records generate toString() automatically. The generated implementation includes the record name and all component values — no boilerplate needed.

// File: RecordToStringDemo.java public class RecordToStringDemo { // Record — compiler generates toString(), equals(), hashCode() automatically record Customer(String customerId, String name, String city) {} record PaymentSummary(String transactionId, double amount, String status) { // Custom toString() — override when generated format is not enough @Override public String toString() { return "[TXN:" + transactionId + " | Rs." + amount + " | " + status + "]"; } } public static void main(String[] args) { Customer customer = new Customer("CUST-501", "Sneha Rao", "Bengaluru"); System.out.println("Auto-generated: " + customer); PaymentSummary payment = new PaymentSummary("TXN-9921", 4999.0, "SUCCESS"); System.out.println("Custom : " + payment); } }
Output:
Auto-generated: Customer[customerId=CUST-501, name=Sneha Rao, city=Bengaluru]
Custom       : [TXN:TXN-9921 | Rs.4999.0 | SUCCESS]

The auto-generated format from records is clean and includes every component by name. Override it only when the format needs to be different — for logging systems that expect a specific pattern, or when some fields need masking.

Basic Example

public class Main { String title; double price; Main(String title, double price) { this.title = title; this.price = price; } @Override public String toString() { return "Book{title='" + title + "', price=" + price + "}"; } public static void main(String[] args) { Main book = new Main("Java Basics", 299.0); System.out.println(book); // implicitly calls toString() System.out.println("Item: " + book); // implicitly calls toString() } }

Output

Book{title='Java Basics', price=299.0}
Item: Book{title='Java Basics', price=299.0}
  1. ›Main overrides toString() to return a readable description of the object's fields.
  2. ›System.out.println(book) calls book.toString() automatically — it does not need to be written explicitly.
  3. ›String concatenation ("Item: " + book) also calls toString() implicitly to convert the object into text.
  4. ›Without the override, both lines would print something meaningless like Main@1b6d3586 instead of the book's details.

Best Practices

Always include the class name at the start. When reading logs with objects from multiple classes, seeing Order{...} versus Product{...} immediately identifies which type is being described. The class name is the first piece of context.

Include fields that answer "which object is this and what state is it in". An order's toString() should include the order ID, status, and amount. It should not include the full shipping address of every item — that is too verbose for a single log line. Think about what a support engineer needs to identify the object and understand its state.

Never include sensitive fields. Passwords, tokens, secret keys, full card numbers, Aadhaar numbers, and similar data must never appear in toString() output. They end up in logs, exception messages, and monitoring dashboards. Mask them or omit them entirely.

Use Objects.toString(field, "null") for nullable nested objects. When a field might be null, field.toString() throws NullPointerException inside your toString() implementation — which silences the original exception and creates a confusing stack trace. Use Objects.toString(field, "null") or check for null explicitly.

Common Mistakes

Mistake 1 — Forgetting to Override toString() on Domain Classes

public class Transaction { private String txnId; private double amount; private String status; Transaction(String txnId, double amount, String status) { this.txnId = txnId; this.amount = amount; this.status = status; } // No toString() override } Transaction txn = new Transaction("TXN-001", 999.0, "PENDING"); System.out.println("Processing: " + txn); // Output: Processing: Transaction@5f4da5c3 // Useless in any log or error message

The fix is one method — the returns in debugging time are immediate and permanent.

Mistake 2 — Calling toString() on a Null Reference Directly

public class OrderProcessor { public void process(Order order) { // If order is null, this throws NullPointerException // inside toString() — confusing stack trace System.out.println("Processing: " + order.toString()); // Safe alternatives: System.out.println("Processing: " + order); // null-safe via concatenation System.out.println("Processing: " + String.valueOf(order)); // explicit null-safe } }

String concatenation ("" + obj) is null-safe — it calls String.valueOf(obj) internally, which returns the string "null" for null references. Calling .toString() directly on a potentially null reference throws immediately.

Mistake 3 — Creating Circular toString() References

public class Department { private String name; private Employee manager; @Override public String toString() { return "Department{name='" + name + "', manager=" + manager + "}"; // manager.toString() calls department.toString() if manager has back-reference // StackOverflowError if circular } } public class Employee { private String name; private Department department; @Override public String toString() { return "Employee{name='" + name + "', dept=" + department + "}"; // department.toString() calls manager.toString() — back to Employee // Infinite recursion — StackOverflowError } }

When two objects hold references to each other and both call the other's toString() inside their own, the result is infinite recursion and a StackOverflowError. Break the cycle by including only the ID or name of the related object rather than the full object reference.

// Fix — use only the name, not the full department object @Override public String toString() { return "Employee{name='" + name + "', dept='" + (department != null ? department.getName() : "null") + "'}"; }

Mistake 4 — Including All Fields Indiscriminately

// Too verbose — dumps everything including fields irrelevant to diagnosis @Override public String toString() { return "User{id=" + id + ", name=" + name + ", email=" + email + ", passwordHash=" + passwordHash // security risk + ", sessionToken=" + sessionToken // security risk + ", createdAt=" + createdAt + ", lastLoginAt=" + lastLoginAt + ", profilePicUrl=" + profilePicUrl + ", addressLine1=" + addressLine1 + ", addressLine2=" + addressLine2 + ", pincode=" + pincode + ", allPreviousOrders=" + allPreviousOrders // extremely verbose + "}"; }

A toString() that dumps every field — including sensitive ones and large collections — creates security risks and log noise. Include only what helps identify the object and understand its current state. Exclude sensitive data unconditionally.

Interview Questions

Q1. What is the toString() method in Java and why should you override it?

toString() is a method inherited from java.lang.Object that returns a string representation of the object. The default implementation returns ClassName@hexHashCode — which is useless for debugging. Java calls it automatically in string concatenation, System.out.println, and logging frameworks. Overriding it to return meaningful field values makes logs readable, error messages informative, and debugging faster. Every domain class should override it.

Q2. When does Java call toString() implicitly?

Java calls toString() implicitly in four situations: when an object is passed to System.out.println() or similar print methods; when an object is used in string concatenation with the + operator; when String.valueOf(obj) is called with an object argument; and when StringBuilder.append(obj) is called with an object. In all these cases, the compiler or runtime converts the object to a string using its toString() method without any explicit call in source code.

Q3. What is the difference between toString(), String.valueOf(), and string concatenation for null objects?

Calling toString() directly on a null reference throws NullPointerException. String.valueOf(null) returns the string "null" safely. String concatenation "" + null also returns "null" safely — the compiler generates a call to String.valueOf() internally. When an object might be null, prefer String.valueOf(obj) or Objects.toString(obj, "default") for safe conversion.

Q4. Should you include all fields in toString()?

No. Include fields that identify the object and describe its current state. Exclude sensitive fields — passwords, tokens, card numbers, secrets — unconditionally. Exclude large collections or deeply nested objects that make the output unreadably verbose. Exclude fields that are irrelevant to the object's identity in the context where it will be logged. The goal is a log entry that tells a developer which object it is and what state it was in — not a dump of every field.

Q5. What is a circular toString() reference and how do you avoid it?

A circular toString() reference occurs when two objects each call toString() on the other inside their own toString() method, causing infinite recursion and a StackOverflowError. This happens with bidirectional object relationships — a Department that holds an Employee and an Employee that holds a Department. The fix is to include only the identifier or name of the related object — not the full object reference — in the toString() output of each side.

Q6. How does toString() work automatically with collections like ArrayList and HashMap?

ArrayList, HashMap, HashSet, and all standard collection classes override toString(). Their implementations iterate over the elements and call toString() on each one to build the output. This means custom domain objects render correctly inside collection output — but only if they override toString(). Without an override, collection output contains the default ClassName@hashCode representations of each element, which is meaningless in logs.

FAQs

Does toString() affect equals() or hashCode() in any way?

No. toString(), equals(), and hashCode() are independent methods from Object. toString() produces a string representation for display. equals() defines logical equality. hashCode() produces a hash for collection bucketing. Overriding toString() has no effect on how the object behaves in HashSet, HashMap, or equality comparisons. Each method serves a different purpose and must be implemented independently.

Can toString() return null?

Technically yes — the method signature allows returning null. But it should never happen. When toString() returns null and the result is concatenated into a string, the string "null" appears. When it is returned to a caller expecting a usable string, it can cause a NullPointerException when the caller calls methods on the result. The contract of toString() is to always return a non-null string.

Do Java records override toString() automatically?

Yes. Records introduced in Java 16 generate a toString() implementation automatically that includes the record name and all component values in the format RecordName[field1=value1, field2=value2]. You can override it in the record body if the auto-generated format does not suit your needs — for example, to mask sensitive components or use a different format.

What is the recommended format for toString() output?

There is no official Java format, but the most widely adopted convention — used by IDEs like IntelliJ for auto-generation — is ClassName{field1='value1', field2=value2, ...}. String values are typically enclosed in single quotes; numeric and boolean values are written directly. The class name at the start and curly braces as delimiters make the output self-documenting and easy to parse visually in log files.

Can IDE auto-generate toString() for you?

Yes. Both IntelliJ IDEA and Eclipse can generate toString() automatically. In IntelliJ, right-click inside the class → Generate → toString(). You can select which fields to include and choose a template format. The auto-generated code uses StringBuilder internally for efficiency. It is a good starting point — review it to ensure sensitive fields are excluded and the format matches what your logging infrastructure expects.

Summary

toString() is the most widely called method in any Java codebase — called automatically every time an object appears in a log, a print statement, or a string expression. The default implementation from Object produces a memory address that carries no domain meaning. Overriding it with meaningful field output makes the entire lifecycle of a feature — development, debugging, production monitoring — faster and clearer.

The rules that matter in practice: include class name and identifying fields, exclude sensitive data without exception, avoid circular references between bidirectional object relationships, and use String.valueOf() instead of direct .toString() calls when null is possible. Records handle this automatically from Java 16 onwards.

For interviews, be ready to explain when Java calls toString() implicitly, why String.valueOf(null) is safer than obj.toString(), and what makes a well-written toString() different from a bad one. These questions appear in both fresher screening rounds and senior design discussions.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →