Skip to content
Java strings 6 min read

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: " + person calls person.toString() behind the scenes.
  • System.out.println(obj)PrintStream.println calls String.valueOf(obj), which calls obj.toString().
  • String formattingString.format("Hello %s", obj) with %s calls obj.toString().
  • StringBuilder.append(obj) — appends the result of obj.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 a null reference throws a NullPointerException. Prefer String.valueOf() or Objects.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 get toString(), equals(), and hashCode() 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:

GuidelineWhy
Include all significant fieldsMakes debugging and logging useful
Keep it side-effect-freeMay be called unexpectedly by debuggers or frameworks
Keep it fastAvoid I/O, locks, or expensive computation
Do not parse it programmaticallyOutput format can change; use getters for logic
Use @OverrideCatches typos at compile time
Handle null fields safelyPrevents 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") + "}";
}
  • Object Class — where toString(), equals(), and hashCode() are defined
  • Strings — the String type that toString() 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 Object method you will commonly override alongside toString()
Last updated June 13, 2026
Was this helpful?