Command Palette

Search for a command to run...

PHASE 6Intermediate Java 5+ ~28 min· topic 6 of 8

Topic 6.6

String Formatting

In one line

String.format, printf and formatted build text from a template with placeholders like %s, %d and %.2f, giving you control over width, alignment, padding, decimals and grouping. Pass a Locale for numbers, because the decimal and thousands separators change by country.

Think of it like this

A form with blanks. A school certificate says "This is to certify that ______ scored ___ marks". The office fills in the blanks for each child, and the blanks have fixed sizes so every certificate lines up. A format string is that form: the fixed text plus blanks (format specifiers), and the arguments are what gets written in each blank.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Format string
A template made of normal text plus placeholders that say where and how to insert values.
Format specifier
One placeholder in a format string, starting with %, like %d or %-10s.
Conversion
The letter at the end of a specifier that says how to display the value: s, d, f, x and so on.
Width
The minimum number of characters a value takes up; extra room is filled with spaces or zeros.
Precision
For decimals, how many digits after the point; for text, the maximum number of characters.
Flag
A symbol in a specifier that changes the style, such as - for left-align or , for thousands separators.
Locale
A language and country setting, such as Locale.US or Locale.GERMANY, that decides number and date styles.
Line separator
The characters that end a line on this operating system: \n on Linux/macOS, \r\n on Windows. %n produces it.

Step by step

01Your first format string

String.format("%s is %d years old", "Asha", 30) scans the template left to right. Each % starts a specifier; the next argument is converted and inserted. The result is Asha is 30 years old.

Arguments are passed as varargs (Object... args, Topic 3.5), so primitives are autoboxed: 30 becomes an Integer. The formatter checks each argument's type against the conversion at run time.

Main.javawhole filejava
String line = String.format("%s is %d years old", "Asha", 30);
System.out.printf("%s is %d years old%n", "Ravi", 25);
String same = "%s is %d years old".formatted("Meera", 28);   // Java 15

02Anatomy of a specifier

Read %2$-10.3s from left to right: argument 2, flag - (left-align), width 10, precision 3, conversion s. So: take the second argument, cut it to 3 characters, and left-align it in a 10-character slot.

Only % and the conversion are required. Everything else is optional, but the order is fixed: index, flags, width, precision, conversion.

Anatomy of a specifierdiagram
Rendering diagram…

03Width and alignment: building a table

To line up columns, give every column a width. Text columns usually look best left-aligned (%-10s); number columns right-aligned (%8.2f), so the decimal points line up.

A width is a minimum. A value longer than the width is never cut; it pushes the rest of the line right. If you need a hard limit on text, add a precision: %-10.10s.

Main.javawhole filejava
System.out.printf("%-10s %5s %8s%n", "Item", "Qty", "Price");
System.out.printf("%-10s %5d %8.2f%n", "samosa", 3, 45.0);
// Item         Qty    Price
// samosa         3    45.00

04Numbers: decimals, rounding, signs and bases

%.2f rounds to 2 decimals using HALF_UP applied to the shortest decimal form of the double (the same digits Double.toString prints). So 0.125 becomes 0.13 and 1.005 becomes 1.01, even though the binary double for 1.005 is very slightly below it. %f with no precision means 6 decimals.

%e gives scientific notation (1.234568e+03). %x, %X and %o show an integer in hex or octal; %#x adds 0x. Negative numbers in hex show the two's-complement bits of the type: String.format("%x", -1) is ffffffff (Topic 1.9).

Formatting never changes the number itself. If you need a rounded value for further maths, round with BigDecimal.setScale or Math.round, not by formatting and parsing.

05Locale: why the same code prints 1,234.50 and 1.234,50

Many countries swap the meaning of . and , in numbers. The formatter uses the locale's decimal separator and grouping separator. With no locale argument it uses Locale.getDefault(Locale.Category.FORMAT), which comes from the operating system or the -Duser.language/-Duser.country flags.

That's a classic production bug: a service writes 3,5 into a CSV on a German server, and the reader on another machine fails to parse it. Machine-readable output should always use Locale.ROOT.

Locale: why the same code prints 1,234.50 and 1.234,50diagram
Rendering diagram…

06When format strings go wrong

The compiler doesn't check format strings (static analysis tools like Error Prone's FormatString check do). Errors appear when the line runs.

Fewer arguments than specifiers throws. More arguments than specifiers is silently allowed: the extras are ignored, which hides bugs. %b is lenient in a different way: null gives false, a Boolean gives its value, and **any other object gives true**, even the String "false".

terminal
$ javac Main.java && java Main
── expected output ──
Exception in thread "main" java.util.MissingFormatArgumentException: Format specifier '%s'
at java.base/java.util.Formatter.format(Formatter.java:2790)
...

07Other tools: DecimalFormat, NumberFormat, MessageFormat, HexFormat

java.text.DecimalFormat formats with a pattern like #,##0.00 and rounds with HALF_EVEN by default (banker's rounding: 0.25 becomes 0.2), unlike %f. NumberFormat.getCurrencyInstance(locale) adds currency symbols and rules.

MessageFormat.format("{0} has {1} items", name, n) uses numbered placeholders, which translators prefer because word order differs between languages. Its gotcha: a single quote starts a quoted section, so write '' for an apostrophe.

java.util.HexFormat (Java 17) formats and parses byte arrays as hex, with optional delimiters, prefixes and upper case, replacing a lot of hand-written loops.

Try it yourself

  1. 1

    Predict the padding

    In the first example, change %8s to %3s and %05d to %2d. Predict each result (remember: width is a minimum), then run.

  2. 2

    Switch the locale

    In the table example, replace every Locale.ROOT with Locale.GERMANY. Predict how the amounts change, then run. Would a program reading this file back with Double.parseDouble still work?

  3. 3

    Find the HALF_UP / HALF_EVEN difference

    Format 0.5, 1.5 and 2.5 with %.0f and with new DecimalFormat("0"). Predict both rows first. Which one would an accountant prefer, and why?

Code & diagrams

The specifiers you'll use every day Java 15+ New tab

formatted() needs Java 15; everything else here works since Java 5.

Sign in to run this example in your browser.

Expected output

Asha is 30 years old
price: 1234.57
grouped: 1,234.57
accounting: (1,234.57)
right [    Asha] left [Asha    ] cut [As]
zero-pad [00042] sign [+42]
hex ff FF 0xff, octal 10
scientific 1.234568e+03, pi [     3.142]
char J, booleans true false, percent 100%
second before first
echo and echo again
Ravi scored 91
A neat table with printf New tab

%-12.6s left-aligns in 12 characters but keeps at most 6. printf is Java 5; the divider uses String.repeat from Java 11.

Sign in to run this example in your browser.

Expected output

Item          Qty    Amount
---------------------------
tea             2     40.00
samosa          3     46.50
paneer tikka    1    180.00
Total                266.50
a-very          1      1.00
Rounding and locales: %f, DecimalFormat, MessageFormat, HexFormat Java 17+ New tab

A single quote in MessageFormat starts a quoted section, which swallowed the {1}. Write '' for an apostrophe.

Sign in to run this example in your browser.

Expected output

%.2f of 0.125: 0.13
%.1f of 0.25:  0.3
%.2f of 1.005: 1.01
DecimalFormat of 0.25 (HALF_EVEN): 0.2
DecimalFormat of 1234567.89: 1,234,567.9
ROOT:    1,234.50
GERMANY: 1.234,50
Asha has 3 items
Ashas cart has {1} items
Asha's cart has 3 items
HexFormat: 1A:2B:FF
Money: format a BigDecimal, don't format a double (fragment)java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;

BigDecimal subtotal = new BigDecimal("199.995");
BigDecimal rounded = subtotal.setScale(2, RoundingMode.HALF_EVEN);   // 200.00, an exact value

String forJson = rounded.toPlainString();                            // "200.00" on every machine
String forPeople = NumberFormat.getCurrencyInstance(Locale.US).format(rounded);  // "$200.00"

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget an argument

Write String.format("%s and %s", "tea").

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.util.MissingFormatArgumentException: Format specifier '%s'
at java.base/java.util.Formatter.format(Formatter.java:2790)
...

Break #2

Use %d for a String

Write String.format("%d items", "three").

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.String
at java.base/java.util.Formatter$FormatSpecifier.failConversion(Formatter.java:4515)
...

Break #3

Invent a conversion

Write String.format("%q", 1).

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.util.UnknownFormatConversionException: Conversion = 'q'
at java.base/java.util.Formatter$FormatSpecifier.conversion(Formatter.java:2958)
...

Myth vs fact

Myth

%.2f rounds like a bank.

Fact

It rounds HALF_UP. DecimalFormat and BigDecimal with RoundingMode.HALF_EVEN give banker's rounding. For money, use BigDecimal and pick the rounding mode on purpose.

Myth

String.format prints the same everywhere.

Fact

Without a locale argument it uses the JVM's default format locale, which changes separators (and on some locales even the digits). Pass Locale.ROOT for machine output.

Myth

\n and %n are the same.

Fact

\n is always a line feed. %n is the platform line separator, \r\n on Windows. Use %n for console and text-file output meant for that platform, \n for network protocols and formats that define LF.

Myth

Extra arguments cause an error.

Fact

Extra arguments are silently ignored. Only missing ones throw, so a forgotten %s can hide a value without any warning.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    String.format parses the format string on every call (it builds a Formatter and a list of specifiers). In hot logging paths prefer parameterised logging (log.info("user {} logged in", id) in SLF4J), which skips formatting entirely when the level is disabled.

  • ▸

    %s calls String.valueOf(arg), except for arguments that implement java.util.Formattable, whose formatTo method is called with the flags, width and precision. That's how a type can control its own %s output, including %#s alternate forms.

  • ▸

    %f on a double uses the FormattedFloatingDecimal algorithm, which starts from the shortest decimal that round-trips (as Double.toString does) and then rounds HALF_UP. That's why 1.005 gives 1.01 in Java but 1.00 in C's printf, which rounds the exact binary value.

  • ▸

    Since Java 21, Locale.of(language, country) is the preferred way to build a locale; the new Locale(...) constructors are deprecated. Locale.ROOT is the neutral locale for language-independent processing.

Remember this

  1. 1

    String.format(template, args...) (Java 5) returns the filled-in String. System.out.printf(template, args...) prints it directly. template.formatted(args...) (Java 15) is the same as String.format(template, args...), written as a method on the template, which reads nicely with text blocks (Topic 6.5). All three use java.util.Formatter underneath.

  2. 2

    Each specifier has the shape %[argument_index$][flags][width][.precision]conversion. The conversion says how to show the value: s text, d whole number, f decimal, e scientific, x/X hex, o octal, c character, b boolean, % a literal percent sign, and n the platform line separator. Upper-case conversions (S, X, E) upper-case the result.

  3. 3

    Width is the minimum number of characters; shorter values are padded with spaces on the left (right-aligned). The **- flag pads on the right instead (left-aligned), which is how you build neat tables. Precision** means decimal places for f and e (%.2f), and the maximum number of characters for s (%.3s cuts to 3).

  4. 4

    Useful flags: 0 pads numbers with zeros (%05d gives 00042), + always shows the sign, , adds thousands grouping (%,d), ( puts negative numbers in brackets (accounting style), # adds a prefix (0x for hex). Argument indexes let you reuse or reorder arguments: %2$s is the second argument and %<s repeats the previous one.

  5. 5

    Locale matters. %,.2f of 1234.5 is 1,234.50 in the US or India, 1.234,50 in Germany. String.format without a locale uses the JVM's default format locale, so the same code prints differently on different servers. For files, logs, JSON and anything a program will read back, pass Locale.ROOT (or use Double.toString). For people, pass their locale.

  6. 6

    Mistakes fail at run time, not compile time: a missing argument throws MissingFormatArgumentException, a wrong type (%d with a String) throws IllegalFormatConversionException, and an unknown letter throws UnknownFormatConversionException. %f rounds with HALF_UP on the value's shortest decimal form. For money and full control, use BigDecimal and DecimalFormat/NumberFormat; for templates with named positions, MessageFormat.

Explain it without notes

01

Describe the full syntax of a format specifier and give an example using every part.

02

What is the difference between width and precision for %s and %f?

03

Why should you pass a Locale to String.format, and which one?

04

How does %.2f round, and how does that differ from DecimalFormat?

05

Which exceptions can formatting throw, and when do you find out about them?

Practice

01

Print a multiplication table for 1 to 4 where every number is right-aligned in a 4-character column.

02

Write static String bar(String label, int percent) that prints label left-aligned in 8 characters, then percent right-aligned in 3 characters followed by %, then that many # characters divided by 10. Print it for cpu 72 and memory 35.

03

Format the number of bytes 1536000 as a human-readable size with one decimal place: 1.5 MB (use 1024-based units and Locale.ROOT).

Trade-offs

  • ↔

    Format strings make layout readable and compact, but they're checked only at run time and parsed on every call. Concatenation is faster and checked by the compiler; use formatting when alignment or number styling matters.

  • ↔

    Locale-aware output is friendly for people and dangerous for machines. Decide per output: Locale.ROOT for data, the user's locale for display.

  • ↔

    %f is fine for display; BigDecimal plus NumberFormat is the only safe path for money, where the rounding rule is a business decision.

Done when you can

  • Done when you can read and write specifiers with index, flags, width, precision and conversion.

  • Done when you can print an aligned table with printf.

  • Done when you always pass Locale.ROOT for machine-readable numbers.

  • Done when you can explain %f rounding versus DecimalFormat and BigDecimal.

  • Done when you recognise the three common formatting exceptions and their causes.

  • Done when you know when to use MessageFormat and HexFormat.