W3docs

Java String Formatting

Format Java strings with String.format and System.out.printf using format specifiers like %s, %d, %f, %n.

Java String Formatting

Concatenation (+) is fine for short, simple strings. The moment you want to pin a number to a specific number of decimal places, line columns up by padding, embed a date with consistent layout, or otherwise format values rather than just paste them in, you reach for the printf-style API that Java borrowed from C.

There are three closely related entry points and you'll see all of them in real code:

  • String.format(fmt, args...) — returns a formatted String.
  • "...".formatted(args...) — instance form added in Java 15. Identical result, friendlier for chaining.
  • System.out.printf(fmt, args...) — prints directly to a PrintStream (or any Formatter).

All three share the same format-specifier syntax. Learn it once.

Format specifiers

A specifier has the shape %[flags][width][.precision]conversion. The conversion letter is the only required piece. The rest tune width, alignment, padding, and precision.

String s = String.format("%-10s | %5d | %8.2f", "apples", 42, 3.14159);
// "apples     |    42 |     3.14"

The most useful conversions:

ConversionArgumentMeaning
%sanytoString() of the value
%dintegraldecimal integer
%ffloating-pointfixed-point decimal
%efloating-pointscientific notation
%gfloating-pointshortest of %e and %f
%x, %ointegralhex / octal
%ccharactersingle character
%banytrue/false (null → "false")
%nnoneplatform-appropriate line separator
%%nonea literal %
%t...date/timea whole family — %tF, %tT, etc.

Width pads the output to at least N characters; precision means "decimal digits" for floats and "max characters" for strings.

Flags: alignment, sign, zero-padding, grouping

A handful of flags fit between % and the width:

  • - — left-align in the width. Default is right-align.
  • 0 — zero-pad to the width (numeric conversions only).
  • + — always show a sign on numbers (+42, -7).
  • (space) — show a leading space for positive numbers, like + but with a blank.
  • , — group digits using the locale's thousands separator.
  • ( — wrap negative numbers in parentheses, accounting-style.
String.format("%08d", 42);        // "00000042"
String.format("%,d", 1234567);    // "1,234,567"
String.format("%+.2f", 3.14159);  // "+3.14"
String.format("%-10s|", "hi");    // "hi        |"

Width and precision

Width is the minimum field width — if the formatted value is wider, nothing is truncated.

Precision means different things for different conversions:

  • %.3f — three digits after the decimal point.
  • %.10s — truncate the string to at most 10 characters.
  • %.4e — four digits of mantissa precision.

Combining width and precision is common when you want columns to align and numbers to be rounded:

String.format("%10.4f", Math.PI);   // "    3.1416"
String.format("%-10.4s", "abcdef"); // "abcd      "

%n vs \n

%n emits the platform-appropriate line separator: "\n" on Unix, "\r\n" on Windows. \n is always exactly one byte. For files and protocol output where the line ending matters precisely, prefer \n and pick it consciously. For console output meant to look right on whichever OS the JVM happens to be running on, %n is the safer choice.

Argument indices: reuse and reorder

A specifier of the form %N$... refers to the N-th argument (1-based). Useful when one value appears more than once in a template, or when the natural reading order differs from the argument order:

String.format("%1$s, %1$s, %1$s!", "go");        // "go, go, go!"
String.format("%2$s before %1$s", "lunch", "tea"); // "tea before lunch"

This is the right tool when localising templates — different languages put nouns in different positions, and a translator can rearrange placeholders without touching the call site.

Locale matters for numbers and dates

Number formatting respects the JVM's default locale unless you override it. In en-US you get 3.14; in de-DE you get 3,14; in fr-FR thousands are grouped with a non-breaking space. For human-facing output, that's usually what you want. For data formats — JSON, CSV, log files, anything machine-readable — it's a disaster waiting for a deployment to Frankfurt.

Always pass a locale explicitly for machine-readable output:

String json = String.format(Locale.ROOT, "{\"price\": %.2f}", 19.95);
// "{\"price\": 19.95}" — always, regardless of JVM locale

Locale.ROOT means "no locale-specific formatting" — period as decimal, no grouping. Locale.US is the other common pick for the same purpose. The dangerous thing is not passing a locale and assuming.

Date/time formatting

%t is a meta-conversion: the letter that follows it picks the field. The same Date, Calendar, Long (millis), or java.time.temporal.TemporalAccessor argument can be formatted in many ways:

LocalDateTime now = LocalDateTime.of(2026, 5, 29, 14, 30, 15);
String.format("%tF",   now);          // "2026-05-29"  — ISO date
String.format("%tT",   now);          // "14:30:15"    — 24-hour time
String.format("%tA",   now);          // "Friday"      — locale-dependent
String.format("%1$tF %1$tT", now);    // "2026-05-29 14:30:15"

For anything beyond quick formatting, the java.time.format.DateTimeFormatter API is more flexible and locale-aware — but %tF and friends remain handy in log lines.

Common pitfalls

  • Wrong conversion for the type. String.format("%d", 3.14) throws IllegalFormatConversionException at runtime — %d wants integral, %f wants floating-point. The compiler can't check it.
  • Missing arguments. Forgetting a placeholder argument throws MissingFormatArgumentException.
  • Locale-dependent decimal in machine output. Covered above.
  • %s on null. Produces "null". Fine for logs, embarrassing in user-facing output.
  • Using + for formatted numbers. "Price: " + 19.95 gives "Price: 19.95", but "Price: " + 0.1 + 0.2 gives "Price: 0.10.2", not "Price: 0.3" — concatenation, not addition.

A worked example

A small order-summary formatter that exercises width, precision, alignment, grouping, locale, and %t date conversion. The output is a tidy two-column report — the kind of thing you'd otherwise build by hand with padLeft helpers.

java— editable, runs on the server

Two things to note: the %<tT in the "Placed" line reuses the previous argument (< is the back-reference flag), avoiding a redundant second placedAt. And the JSON line uses Locale.ROOT — the same code on a German JVM still emits 1339.43, not 1339,43, which is exactly what a JSON parser expects.

What's next

Building strings is one half of the job. Comparing them — equality, ordering, case folding, the difference between == and equals — is the other half and a frequent source of subtle bugs. Continue to Java String comparison.

Practice

Practice

You want to format a `double` total as a string with a thousands separator and exactly two decimal places, and you need the result to use a period as the decimal separator regardless of the JVM's locale (so a JSON consumer can parse it). Which call does that correctly?