Formatter and Format Specifiers
Objective
String.format, printf, and Formatter all funnel through the same conversion engine: a format string made of literal characters plus %-prefixed specifiers, each specifier consuming one argument in order and rendering it according to a conversion character (%d, %s, %f, ...) plus optional flags for width, precision, padding, and grouping. Most of this is discoverable by example, but a handful of specifics — argument indexing, the comma/space/(/0 flags, and the difference between width and precision — are genuinely easy to get wrong or forget, and are exactly the parts worth having a clear reference for.
Use Cases
- Producing aligned, human-readable console/log output — right- or left-justified columns of numbers, consistent decimal places, grouped thousands.
- Formatting currency or measurement output with explicit sign display (
+100vs.100) or accounting-style negative numbers ((100)instead of-100). - Reusing the same argument in multiple places in one format string (a date formatted as day, then month, then year, all from one
Calendar/TemporalAccessorargument) without repeating it in the call. - Building an internal
Formatterexplicitly (rather than throughString.format) when the output needs to accumulate into a specificAppendable, or be written straight to a file via one ofFormatter's file-backed constructors.
Deep Dive
The three ways to reach the same engine
javaString s = String.format("%s scored %d%%", "Ana", 92); // one-shot, returns a String
System.out.printf("%s scored %d%%%n", "Ana", 92); // writes straight to System.out
Formatter fmt = new Formatter(); // explicit Formatter, own buffer
fmt.format("%s scored %d%%", "Ana", 92);
String result = fmt.toString();
fmt.close();printf (on PrintStream/PrintWriter) and String.format are both thin wrappers over Formatter — reach for the explicit form only when you need control String.format doesn't give you: writing directly to a file via one of Formatter's file-backed constructors, or accumulating into an existing Appendable/StringBuilder across multiple format() calls.
Conversions: one character decides the shape
javaString.format("%d", 42); // 42 — integer, decimal
String.format("%f", 3.14159); // 3.141590 — floating-point, 6 decimals by default
String.format("%e", 12345.6); // 1.234560e+04 — scientific notation
String.format("%x", 250); // fa — hexadecimal
String.format("%X", 250); // FA — uppercase variant
String.format("%o", 250); // 372 — octal
String.format("%s", "hi"); // hi — any object, via toString()
String.format("%c", 'z'); // z — a single characterJava type-checks each argument against its specifier — %d on a double argument throws IllegalFormatConversionException rather than silently coercing, unlike C's printf, which trusts the format string and reads memory according to it regardless of what was actually passed.
Width, precision, and the difference between them
javaString.format("[%10.4f]", 10.12345); // [ 10.1235] — width 10, 4 decimal places
String.format("[%-10.4f]", 10.12345); // [10.1235 ] — left-justified in the same field
String.format("[%5.7s]", "hi"); // [ hi] — width 5 (padded), max length 7Width is the minimum total field length (padded with spaces, or 0s if the 0 flag is used); precision means something different per conversion — decimal places for %f/%e, significant digits for %g, and maximum string length for %s (truncating, not padding, if the string is longer). The two numbers sit in the same %width.precision position but answer different questions, which is the part most easily misremembered.
The flags that carry real weight
javaString.format("%+d", 100); // +100 — always show the sign
String.format("% d", 100); // " 100" — leading space for positive, aligns with "-100"
String.format("%(d", -100); // (100) — accounting-style negative, no minus sign
String.format("%05d", 42); // 00042 — zero-padded instead of space-padded
String.format("%,.2f", 4356783497.34); // 4,356,783,497.34 — grouping separator
String.format("%#x", 250); // 0xfa — # prefixes hex with 0x, octal with a leading 0, (grouping), +/space (sign display), ( (parenthesized negatives), and 0 (zero-padding) are the flags worth having memorized — most everyday formatting needs some combination of these rather than the bare conversion character alone.
Argument index and relative index: reuse without repeating
javaString.format("%3$d %1$d %2$d", 10, 20, 30); // "30 10 20" — explicit n$ index, 1-based
String.format("%d in hex is %1$x", 255); // "255 in hex is ff" — reuse argument 1
String.format("%d in hex is %<x", 255); // same result — "<" reuses the PREVIOUS argumentAn explicit n$ right after the % overrides left-to-right matching entirely; < is shorthand for "the argument the previous specifier just used." This is most valuable formatting the same value multiple ways in one call — a %t-based date/time format that needs day, month, and year all pulled from one Calendar/TemporalAccessor argument is the canonical case: %< lets that one argument be passed once and referenced repeatedly, instead of appearing three times in the argument list.
%n vs. \n
javaSystem.out.printf("line one%nline two%n");%n inserts the platform's own line separator (\r\n on Windows, \n elsewhere) the same way System.lineSeparator() does; a literal \n in the format string always inserts exactly \n regardless of platform. %n is the portable choice specifically inside a format string; %% is the matching escape for a literal % character, since a bare % in a format string is otherwise parsed as the start of a specifier.
Trade-offs
Formattertype-checks strictly, which surfaces bugs early but breaks on any mismatch — passing anIntegerwhere%fexpects a floating type throws immediately rather than silently misformatting, which is safer than C'sprintfbut means a format string and its argument list must stay in exact sync as code evolves; a refactor that changes an argument's type without updating the specifier fails at run time, not compile time.- Locale matters and is easy to forget.
%,d's grouping character, decimal points, and date/time formatting all depend on the active locale —Formatter's no-locale constructor uses the JVM default, which differs across environments; an explicitLocaleargument (String.format(Locale.US, "%,.2f", amount)) is the only way to guarantee identical output regardless of where the code runs. Seeresource-bundles-and-localefor the full locale-resolution picture. - Width/precision on
%struncates strings silently — a display string longer than the specified precision loses its tail with no indication in the output that truncation happened, which is fine for a fixed-width report column and a real bug source if the full string mattered. - An explicitly constructed
Formatterholds a resource and should be closed (it implementsAutoCloseable) — especially when file-backed, where an unclosedFormattercan leave buffered output unflushed;String.format/printfnever expose this concern because they manage their own internalFormatterfor you. - Relative indexing (
%<) only reuses the immediately preceding specifier's argument — it isn't a general "go back N arguments" mechanism, so reordering specifiers in a format string can silently change which argument%<now refers to; an explicitn$index is the more robust choice once a format string gets complex enough that reordering is likely. - An explicit
Formatteris not thread-safe. It buffers its output in internal mutable state, so sharing one instance across threads (e.g. caching it in astaticfield to "save resources") corrupts output under concurrent calls.String.format/printfnever hit this because each call constructs its own privateFormatterinternally.javastatic final Formatter shared = new Formatter(); // unsafe: concurrent format() calls race on the buffer // safe alternative: no shared state to race on String s = String.format("%s scored %d%%", name, score);