What do String.format() and String.join() do, and when would you choose each over manual concatenation?
answer
- format = printf template: %s %d %f %n, width/.precision/flags
- format is locale-aware → use Locale.ROOT for machine output
- join(delim, parts) → delimiter only BETWEEN, no trailing
- join avoids the trailing-separator bug; takes Iterable too
- loops → StringBuilder or Collectors.joining, not format/+
basics
~10 sString.format() builds a string from a template with placeholders like %s and %d filled by arguments. String.join() glues several strings together with a separator between them.
solid answer
~40 sString.format(template, args...) returns a formatted string using printf-style conversion specifiers: %s for any object, %d for integers, %f for floats, %n for a platform newline, and width/precision/flags like %,.2f or %-10s. It's locale-aware (an overload takes a Locale), so number/date formatting can vary; use Locale.ROOT for stable machine output. String.join(delimiter, elements) concatenates strings (or any CharSequence/Iterable) placing the delimiter only between elements, never at the ends — ideal for CSV-like output without trailing-separator bugs. Choose format when you need typed conversions, padding, or locale formatting; choose join for simple delimiter-separated lists; for building strings in a loop use StringBuilder or a Stream Collectors.joining, since both format (parses the pattern each call) and repeated '+' concatenation are comparatively costly in hot paths.
code
java · 7 linesString.format(Locale.ROOT, "%-6s|%,.2f%n", "sum", 1234.5);
// "sum |1,234.50\n"
String.join(", ", List.of("a", "b", "c")); // "a, b, c"
// Stream equivalent with wrapping:
List.of("a","b").stream().collect(Collectors.joining(", ", "[", "]")); // "[a, b]"go deeper
Knows format fills %s/%d placeholders and join puts a separator between strings.
Uses width/precision/flags, knows join avoids trailing separators and accepts an Iterable, and reaches for StringBuilder in loops.
Explains locale sensitivity and Locale.ROOT, runtime IllegalFormatException, Collectors.joining, and the performance trade-offs vs StringBuilder/+.
Establishes conventions for deterministic formatting (locale policy), efficient string building at scale, and chooses the right tool per call site in reviews.
## String.format — templated, typed formatting `String.format` produces a new string by filling a **template** with values. The template contains **format specifiers** (placeholders) introduced by `%`. The general shape is `%[flags][width][.precision]conversion`: - **conversion** — what kind of value: `%s` (any object, via its `toString`), `%d` (decimal integer), `%f` (floating point), `%x` (hex), `%b` (boolean), `%c` (char), `%n` (a platform-correct line separator — prefer it over `\n`), and `%%` for a literal percent sign. - **width** — minimum field width, e.g. `%10s` right-pads to 10 columns; `%-10s` left-justifies. - **precision** — for floats, digits after the decimal: `%.2f` → two decimals. - **flags** — e.g. `,` for grouping (`%,d` → `1,000,000`), `0` for zero-padding, `+` to force a sign. Example: `String.format("%-8s|%,.2f%n", "total", 1234.5)` → `"total |1,234.50"` plus a newline. **Locale sensitivity:** number and date formatting depend on a `Locale`. The plain `format(fmt, args)` uses the JVM's **default locale**, so `%,.2f` might render `1.234,50` in some locales. For deterministic, machine-readable output use the overload `String.format(Locale.ROOT, fmt, args)`. Forgetting this is a classic source of environment-dependent bugs. **Errors:** a mismatch between specifier and argument type (e.g. `%d` given a String) throws `IllegalFormatException` at runtime — format strings are **not** checked at compile time. ## String.join — delimiter-separated concatenation `String.join(delimiter, elements...)` glues pieces together with the **delimiter placed only between** elements, not at the start or end. Overloads accept varargs of `CharSequence` or an `Iterable<? extends CharSequence>` (so a `List<String>` works directly): - `String.join(", ", "a", "b", "c")` → `"a, b, c"`. - `String.join("/", List.of("usr", "local", "bin"))` → `"usr/local/bin"`. The key benefit: it avoids the **trailing-separator bug** you hit when manually doing `s += part + ","` in a loop and then stripping the last comma. A single element yields just that element (no delimiter); an empty collection yields `""`. ## When to choose which - **format** — you need typed conversions, alignment/padding, fixed decimals, or locale-specific rendering. The cost: it parses the format pattern each call. - **join** — you have a list of strings and want them separated by a fixed delimiter, cleanly handling the boundaries. - **`+` concatenation** — fine for a few literals; the compiler turns `a + b + c` into efficient code (often `StringConcatFactory` / `invokedynamic` on modern JDKs). But inside a **loop** it creates many throwaway strings. - **StringBuilder** — the right tool for building a string incrementally in a loop. - **Collectors.joining(delimiter, prefix, suffix)** — the Stream equivalent of join with optional wrapping, e.g. `list.stream().map(...).collect(Collectors.joining(", ", "[", "]"))`. ## Related: formatted (Java 15+) and text blocks Since Java 15, `someTemplate.formatted(args)` is an instance shorthand for `String.format(someTemplate, args)`, which reads nicely with text blocks for multi-line templates.
- Why might String.format("%,.2f", 1234.5) produce different output on two machines?format uses the default Locale, and grouping/decimal symbols vary by locale (e.g. '1,234.50' vs '1.234,50'). Pass Locale.ROOT (or a fixed Locale) for deterministic output.
- How would you join a List<String> with commas and wrap it in brackets?Either String.join(", ", list) then add brackets, or in one step list.stream().collect(Collectors.joining(", ", "[", "]")), which adds the prefix/suffix for you.
saying these in an interview costs you the question
- Using default-locale format for machine-readable numbers/IDs
- Expecting format strings to be compile-time type-checked
- Thinking join adds a delimiter at the ends
- Using String.format or '+' inside hot loops instead of StringBuilder
- Using \n instead of %n / line.separator for portable newlines