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%dor%-10s. - Conversion
- The letter at the end of a specifier that says how to display the value:
s,d,f,xand 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.USorLocale.GERMANY, that decides number and date styles. - Line separator
- The characters that end a line on this operating system:
\non Linux/macOS,\r\non Windows.%nproduces 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.
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 1502Anatomy 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.
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.
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.0004Numbers: 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.
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".
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
Predict the padding
In the first example, change
%8sto%3sand%05dto%2d. Predict each result (remember: width is a minimum), then run. - 2
Switch the locale
In the table example, replace every
Locale.ROOTwithLocale.GERMANY. Predict how the amounts change, then run. Would a program reading this file back withDouble.parseDoublestill work? - 3
Find the HALF_UP / HALF_EVEN difference
Format
0.5,1.5and2.5with%.0fand withnew DecimalFormat("0"). Predict both rows first. Which one would an accountant prefer, and why?
Code & diagrams
formatted() needs Java 15; everything else here works since Java 5.
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%-12.6s left-aligns in 12 characters but keeps at most 6. printf is Java 5; the divider uses String.repeat from Java 11.
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.00A single quote in MessageFormat starts a quoted section, which swallowed the {1}. Write '' for an apostrophe.
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:FFimport 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").
Break #2
Use %d for a String
Write String.format("%d items", "three").
Break #3
Invent a conversion
Write String.format("%q", 1).
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.formatparses the format string on every call (it builds aFormatterand 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. - ▸
%scallsString.valueOf(arg), except for arguments that implementjava.util.Formattable, whoseformatTomethod is called with the flags, width and precision. That's how a type can control its own%soutput, including%#salternate forms. - ▸
%fon adoubleuses theFormattedFloatingDecimalalgorithm, which starts from the shortest decimal that round-trips (asDouble.toStringdoes) and then rounds HALF_UP. That's why1.005gives1.01in Java but1.00in C'sprintf, which rounds the exact binary value. - ▸
Since Java 21,
Locale.of(language, country)is the preferred way to build a locale; thenew Locale(...)constructors are deprecated.Locale.ROOTis the neutral locale for language-independent processing.
Remember this
- 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 asString.format(template, args...), written as a method on the template, which reads nicely with text blocks (Topic 6.5). All three usejava.util.Formatterunderneath. - 2
Each specifier has the shape
%[argument_index$][flags][width][.precision]conversion. The conversion says how to show the value:stext,dwhole number,fdecimal,escientific,x/Xhex,ooctal,ccharacter,bboolean,%a literal percent sign, andnthe platform line separator. Upper-case conversions (S,X,E) upper-case the result. - 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 forfande(%.2f), and the maximum number of characters fors(%.3scuts to 3). - 4
Useful flags:
0pads numbers with zeros (%05dgives00042),+always shows the sign,,adds thousands grouping (%,d),(puts negative numbers in brackets (accounting style),#adds a prefix (0xfor hex). Argument indexes let you reuse or reorder arguments:%2$sis the second argument and%<srepeats the previous one. - 5
Locale matters.
%,.2fof 1234.5 is1,234.50in the US or India,1.234,50in Germany.String.formatwithout 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, passLocale.ROOT(or useDouble.toString). For people, pass their locale. - 6
Mistakes fail at run time, not compile time: a missing argument throws
MissingFormatArgumentException, a wrong type (%dwith a String) throwsIllegalFormatConversionException, and an unknown letter throwsUnknownFormatConversionException.%frounds with HALF_UP on the value's shortest decimal form. For money and full control, useBigDecimalandDecimalFormat/NumberFormat; for templates with named positions,MessageFormat.
Explain it without notes
Describe the full syntax of a format specifier and give an example using every part.
What is the difference between width and precision for %s and %f?
Why should you pass a Locale to String.format, and which one?
How does %.2f round, and how does that differ from DecimalFormat?
Which exceptions can formatting throw, and when do you find out about them?
Practice
Print a multiplication table for 1 to 4 where every number is right-aligned in a 4-character column.
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.
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.ROOTfor data, the user's locale for display. - ↔
%fis fine for display;BigDecimalplusNumberFormatis 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.ROOTfor machine-readable numbers.Done when you can explain
%frounding versusDecimalFormatandBigDecimal.Done when you recognise the three common formatting exceptions and their causes.
Done when you know when to use
MessageFormatandHexFormat.