String Format
String.format builds a string from a format string containing placeholders, called format specifiers, and a list of values to put into them. It controls exactly how each value appears: how many decimal places, how wide a column is, whether numbers have thousands separators, how dates are written. The same mini-language is used by System.out.printf, String.formatted, PrintWriter.printf, and java.util.Formatter.
Three Ways to Format
String name = "Ada";
double total = 1499.5;
String a = String.format("Hi %s, your total is %.2f", name, total);
String b = "Hi %s, your total is %.2f".formatted(name, total); // Java 15+
System.out.printf("Hi %s, your total is %.2f%n", name, total); // prints directly
All three produce Hi Ada, your total is 1499.50. formatted is an instance method on the format string itself; printf writes to the stream instead of returning a string.
Anatomy of a Format Specifier
%[argument_index$][flags][width][.precision]conversion
| Part | Example | Meaning |
|---|---|---|
% | Starts a specifier | |
argument_index$ | 2$ | Use the 2nd argument instead of the next one |
| flags | -, 0, +, ,, ( | Alignment, padding, sign, grouping |
| width | 10 | Minimum number of characters |
.precision | .2 | Decimal places for numbers, maximum length for strings |
| conversion | d, f, s, ... | The type of value and how to render it |
Only % and the conversion are required. The order of the optional parts is fixed: writing %12,.2f instead of %,12.2f is an error.
Conversions
| Conversion | Type | Example | Result |
|---|---|---|---|
%s | Any value (toString) | format("%s", List.of(1, 2)) | [1, 2] |
%S | Any value, uppercased | format("%S", "hi") | HI |
%d | Integers (int, long, BigInteger, ...) | format("%d", 42) | 42 |
%f | Floating point, fixed notation | format("%.3f", Math.PI) | 3.142 |
%e | Scientific notation | format("%.3e", 12345.678) | 1.235e+04 |
%x / %X | Hexadecimal | format("%x", 255) | ff |
%o | Octal | format("%o", 8) | 10 |
%c | Character | format("%c", 'z') | z |
%b | Boolean (false for null) | format("%b", null) | false |
%n | Platform line separator | \n or \r\n | |
%% | A literal percent sign | format("%d%%", 45) | 45% |
%f without a precision shows six decimal places: format("%f", 2.5) gives 2.500000.
The argument type must suit the conversion. %d with a double throws IllegalFormatConversionException; too few arguments throws MissingFormatArgumentException. These are runtime errors, so test format strings, especially rarely used ones such as error messages.
Width and Alignment
Width sets the minimum number of characters. Values are right-aligned by default; the - flag left-aligns them:
String.format("[%5d]", 42); // [ 42]
String.format("[%-5d]", 42); // [42 ]
String.format("[%8s]", "ab"); // [ ab]
String.format("[%-8s]", "ab"); // [ab ]
String.format("[%10.3f]", Math.PI); // [ 3.142]
Longer values are never cut; they simply exceed the width. For strings, precision truncates: format("%.3s", "abcdef") gives abc.
Width and alignment are how console reports and text receipts line up their columns.
Flags
| Flag | Effect | Example | Result |
|---|---|---|---|
- | Left-align within the width | %-6d with 42 | 42 |
0 | Pad numbers with leading zeros | %05d with 42 | 00042 |
+ | Always show the sign | %+d with 42 | +42 |
, | Locale-specific grouping separators | %,d with 1000000 | 1,000,000 |
( | Negative numbers in parentheses | %(,.2f with -1234.5 | (1,234.50) |
(space) | A leading space for positive numbers | % d with 42 | 42 |
# | Alternate form (0x prefix for hex) | %#x with 255 | 0xff |
Flags combine: %,12.2f is "group thousands, at least 12 wide, 2 decimals." %08.2f with 3.14159 gives 00003.14.
Argument Indexes
Arguments are normally consumed in order. An explicit index (n$) selects a specific argument, and < reuses the previous one:
String.format("%2$s %1$s %2$s", "a", "b"); // "b a b"
String.format("%tY-%<tm-%<td", LocalDate.of(2026, 11, 12)); // "2026-11-12"
Locales
Decimal points, grouping separators, and month names depend on the locale. Without one, String.format uses the JVM's default locale, so the same code can produce different output on different machines:
String.format(Locale.US, "%,.2f", 1234567.891); // 1,234,567.89
String.format(Locale.GERMANY, "%,.2f", 1234567.891); // 1.234.567,89
Pass an explicit locale whenever the output is read by programs (CSV files, APIs, logs), so that a decimal comma never appears where a decimal point is expected. For currency amounts shown to users, NumberFormat.getCurrencyInstance(locale) adds the currency symbol and correct conventions; the Number Format lesson covers it.
Dates and Times
The %t conversions format date and time values, but for java.time types a DateTimeFormatter is usually clearer:
LocalDate d = LocalDate.of(2026, 11, 12);
String.format("%tB %<td, %<tY", d); // November 12, 2026
d.format(DateTimeFormatter.ofPattern("dd MMM yyyy")); // 12 Nov 2026
Date formatting has its own lesson.
Format Versus Concatenation
String a = "Item: " + name + " | Qty: " + qty + " | Price: " + price;
String b = String.format("Item: %-12s | Qty: %3d | Price: %,10.2f", name, qty, price);
Formatting is preferable when output needs alignment, fixed decimals, padding, or grouping, and when the overall shape of a message should be easy to see. Concatenation is simpler for short messages with no formatting needs. Formatting is somewhat slower than concatenation because the format string is parsed each time, which only matters in very hot code paths.
Practical Example: A Formatted Payslip
import java.time.LocalDate;
import java.util.*;
public class Payslip {
record Item(String label, double amount) { }
public static void main(String[] args) {
String name = "Meena Raghavan";
LocalDate month = LocalDate.of(2026, 10, 1);
List<Item> earnings = List.of(new Item("Basic", 62000), new Item("HRA", 24800), new Item("Special allowance", 13450.5));
List<Item> deductions = List.of(new Item("Provident fund", 7440), new Item("Professional tax", 208.33), new Item("Income tax", 9875));
String row = "%-22s %12s%n";
String money = "%-22s %,12.2f%n";
StringBuilder out = new StringBuilder();
out.append(String.format("PAYSLIP %tB %<tY%n", month));
out.append(String.format("Employee: %s (ID %06d)%n", name, 4217));
out.append("-".repeat(35)).append('\n');
out.append(String.format(row, "Earnings", "INR"));
double gross = 0;
for (Item i : earnings) { out.append(String.format(money, i.label(), i.amount())); gross += i.amount(); }
out.append(String.format(row, "Deductions", ""));
double ded = 0;
for (Item i : deductions) { out.append(String.format(money, i.label(), -i.amount())); ded += i.amount(); }
out.append("-".repeat(35)).append('\n');
out.append(String.format(money, "Net pay", gross - ded));
out.append(String.format("Deductions are %.1f%% of gross pay%n", ded / gross * 100));
System.out.print(out);
}
}
Output (with an English default locale):
PAYSLIP October 2026
Employee: Meena Raghavan (ID 004217)
-----------------------------------
Earnings INR
Basic 62,000.00
HRA 24,800.00
Special allowance 13,450.50
Deductions
Provident fund -7,440.00
Professional tax -208.33
Income tax -9,875.00
-----------------------------------
Net pay 82,727.17
Deductions are 17.5% of gross pay
Two reusable format strings, row and money, give every line the same layout: a 22-character left-aligned label followed by a 12-character right-aligned value. Because the decimal places and widths are fixed, the amounts line up in a column regardless of their size. Other specifiers used here:
%,12.2fadds grouping separators and two decimals.%06dzero-pads the employee ID.%tB %<tYprints the month name and year from a singleLocalDateargument, using<to reuse it.%%produces the literal percent sign.
On a machine with a different default locale, the separators and month name would change; passing a Locale as the first argument to String.format would fix them.