skip to content

How do records, enums, arrays, and standard collections behave with toString(), and when must you still override it?

level: seniorimportance: nice to knowfreq 30%

answer

  1. record -> Name[c=v, ...] free
  2. enum default toString = name()
  3. collections print [..]/{..} recursively
  4. arrays DON'T override -> [I@hex
  5. Arrays.toString / deepToString for arrays

basics

~20 s

Records, enums, and most collections give you a useful toString() automatically (e.g. Point[x=3, y=4], the enum constant name, [a, b, c]). Plain arrays don't — they print the default class@hash, so use Arrays.toString() or Arrays.deepToString() for them.

solid answer

~40 s

Several JDK types provide good toString() so you don't override: a record auto-generates ClassName[component=value, ...]; an enum's toString() returns the constant's name() by default (overridable); String returns itself; the wrapper types and most collections (ArrayList, HashMap, etc.) print readable forms like [a, b] or {k=v}. The big exception is plain arrays: int[] or Object[] inherit Object.toString() and print something like [I@1b6d3586, because arrays don't override it — use Arrays.toString(arr) for one dimension and Arrays.deepToString(arr) for nested arrays. You still write your own toString() for ordinary domain classes that aren't records, when you want a format different from a record's default, or to deliberately omit sensitive fields (a record's generated toString prints every component, including secrets, so you'd override it there too). Collections call element toString() recursively, so beware a self-referential collection printing itself.

code

java · 13 lines
java
record Point(int x, int y) {}
enum Color { RED, GREEN }

Point p = new Point(3, 4);
System.out.println(p);                    // Point[x=3, y=4]   (record-generated)
System.out.println(Color.RED);            // RED               (enum default = name())

int[] nums = {1, 2, 3};
System.out.println(nums);                 // [I@1b6d3586       (array default - NOT useful)
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3]

int[][] grid = {{1, 2}, {3, 4}};
System.out.println(java.util.Arrays.deepToString(grid)); // [[1, 2], [3, 4]]

go deeper

for a junior

Knows printing an array gives gibberish and you use Arrays.toString(), and that records print their fields automatically.

for a middle

Distinguishes which JDK types give a good toString() (records, enums, collections) from arrays which don't, and uses deepToString for nested arrays.

for a senior

Notes the secret-leak risk of a record's generated toString, the enum toString-vs-name()/valueOf mismatch, and recursive element printing in collections.

for a principal

Establishes conventions for records vs hand-rolled toString across a codebase, treats generated toString as a PII/secret-leak surface, and weighs display labels vs name() round-tripping for enums in serialization boundaries.

## The landscape of who gives you a good toString() Not every type needs a hand-written `toString()`. Knowing which ones are covered saves work and avoids surprises. ### Records (Java 16+) A **record** auto-generates a `toString()` of the form `ClassName[component1=value1, component2=value2, ...]`, listing every component in declaration order. Example: `record Point(int x, int y) {}` yields `Point[x=3, y=4]`. You can override it, but usually don't need to. **Caveat:** the generated version prints **every** component, so if a record holds a secret (password, token), its default `toString()` leaks it — override `toString()` to exclude it. ### Enums Every enum's default `toString()` returns the **constant name** — the same text as `name()`. So `Color.RED.toString()` is `"RED"`. You *can* override `toString()` on an enum for display purposes (e.g. return `"Red"`), but be careful: `valueOf(String)` matches `name()`, not your overridden `toString()`, so a custom `toString()` won't round-trip through `valueOf` unless you write your own parser. ### String and wrappers `String.toString()` returns the string itself. The wrapper types (`Integer`, `Boolean`, `Double`, …) print their value (`"42"`, `"true"`). `Optional`, `Path`, `BigDecimal`, etc. all have sensible `toString()`. ### Collections and maps The standard collections override `toString()`: - `List`/`Set`: `[a, b, c]`. - `Map`: `{k1=v1, k2=v2}`. Crucially they call `toString()` on **each element**, recursively. So a `List<Point>` prints its points using `Point`'s `toString()`. A **self-referential** collection (a list that contains itself) can recurse — the JDK guards some cases by printing `(this Collection)`, but custom structures can still `StackOverflowError`. ### Arrays — the classic trap A Java **array is an object**, but the array classes do **not** override `toString()`. So `new int[]{1,2,3}.toString()` prints something like `[I@1b6d3586` (`[I` = "array of int", then `@hash`). To get readable output: - **`Arrays.toString(arr)`** → `[1, 2, 3]` for a one-dimensional array. - **`Arrays.deepToString(arr)`** → handles **nested** arrays (`int[][]`, `Object[]` containing arrays), e.g. `[[1, 2], [3, 4]]`. This is why printing an array directly (`System.out.println(myArray)`) is a frequent beginner bug. ## When you still must (or should) override toString() 1. **Ordinary classes** that aren't records and carry meaningful state. 2. **A record whose default format is wrong for you** — e.g. you want a different layout, or to **hide a sensitive component**. 3. **An enum** when you want a human-facing label different from the constant name (mind the `valueOf` mismatch). 4. Never for arrays directly — instead **call `Arrays.toString`/`deepToString`** at the use site, since you can't add methods to the array type. ## Quick reference | Type | Default toString() | Action | |---|---|---| | record | `Name[c=v, ...]` | usually none; override to hide secrets | | enum | constant name | override only for display | | String/wrappers | the value | none | | List/Set/Map | `[...]` / `{...}` | none | | array | `[I@hex` etc. | use `Arrays.toString`/`deepToString` | | your domain class | `Class@hex` | override |

  • Why does printing an int[] show something like [I@1b6d3586?
    Arrays are objects but don't override Object.toString(), so you get the default class-name ("[I" means int-array) plus @hash. Use Arrays.toString() for one dimension or Arrays.deepToString() for nested arrays.
  • If a record component holds a password, what's the toString() risk?
    The auto-generated toString() prints every component, so the password would appear in logs. Override toString() on the record to exclude the sensitive component.

saying these in an interview costs you the question

  • Expecting System.out.println(intArray) to print the elements
  • Forgetting a record's generated toString prints secret components
  • Assuming enum valueOf round-trips through an overridden toString
  • Using Arrays.toString on a nested array instead of deepToString

context