JAVA / STRINGS AND TEXT HANDLING
Formatting output with format and printf style
Compose aligned, fixed-precision text with printf and String.format, controlling width, padding, rounding and locale.
What you will learn
- Read one specifier as %[argIndex$][flags][width][.precision]conversion
- Align columns with %-12s and %8.2f instead of hand-counted spaces
- Remember width only pads, while .precision truncates %s and rounds %f
- Pass Locale.ROOT or Locale.US when the formatted number will be parsed again
Understanding Formatting output with format and printf style
Both System.out.printf and String.format hand the format string plus the arguments to a java.util.Formatter, which walks the text and copies literal characters straight through until it hits a %. Everything from that % up to the conversion letter is a small instruction with the shape %[argIndex$][flags][width][.precision]conversion. The only real difference between the two entry points is the destination: printf writes to the PrintStream, while format returns a new String you can store, log, or measure.
Width and precision are asymmetric, and that is where most surprises come from. Width is a minimum field size, so the formatter pads with spaces (right-aligned by default, left-aligned with -, zero-filled for numbers with 0) but never discards characters, which means one over-long value silently pushes a whole column out of line. Precision means something different per conversion: on %f it is the exact number of fraction digits, rounded half up, and on %s it is a maximum character count that does truncate.
A format string is data, not code, so the compiler checks nothing inside it. String.format("%d", 19.5) compiles happily and then throws IllegalFormatConversionException the moment it runs, and a stray % in ordinary prose fails the same way. Numeric conversions also consult a Locale, taken from Locale.getDefault(Locale.Category.FORMAT) unless you pass one explicitly, so %,.2f can produce 1,234.50 or 1.234,50 depending on where the JVM thinks it is running.
public class ReceiptLines {
public static void main(String[] args) {
System.out.printf("%-10s %3d x %6.2f = %7.2f%n", "Espresso", 3, 2.5, 7.5);
String line = String.format("%-10s %3d x %6.2f = %7.2f", "Croissant", 12, 1.75, 21.0);
System.out.println(line);
System.out.println("width: " + line.length());
System.out.printf("tax rate %+.1f%%%n", 8.5);
}
}Each %-specifier is a runtime instruction naming a conversion that must match the argument's type, a minimum width that pads, and a precision that rounds or truncates.
Worked examples
Conversions are checked at run time
Shows how the same double behaves under %s and %f, and what happens when the conversion does not fit the argument type.
import java.util.IllegalFormatConversionException;
public class FormatTypes {
public static void main(String[] args) {
double amount = 19.5;
System.out.println(String.format("%s", amount));
System.out.println(String.format("%.0f", amount));
try {
System.out.println(String.format("%d", amount));
} catch (IllegalFormatConversionException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}Example explained
Line 1%s on a boxed Double just calls String.valueOf, so you see Double.toString output unchanged: 19.5.
Line 2%.0f rounds half up to 20 and omits the decimal separator entirely, because a precision of 0 asks for zero fraction digits.
Line 3%d never coerces a double to an integer; the mismatch is only discovered when Formatter compares the conversion letter with the argument's class at run time.
Line 4The message 'd != java.lang.Double' names the conversion that failed and the type it was handed, which is usually enough to spot the wrong specifier.
The same number in three locales
Demonstrates that the decimal separator and grouping separator come from the Locale, not from the format string.
import java.util.Locale;
public class FormatLocale {
public static void main(String[] args) {
double price = 1234567.891;
System.out.println(String.format(Locale.US, "%,.2f", price));
System.out.println(String.format(Locale.GERMANY, "%,.2f", price));
System.out.println(String.format(Locale.ROOT, "%.2f", price));
}
}Example explained
Line 1The , flag asks for grouping; Locale.US fills in ',' as the group separator and '.' as the decimal separator.
Line 2Locale.GERMANY swaps both symbols, so identical code and identical input produce 1.234.567,89.
Line 3Locale.ROOT with no , flag gives 1234567.89, a form Double.parseDouble can read back on any machine.
Line 4Dropping the Locale argument does not mean 'no locale': it silently uses the JVM default, which differs between machines.
Flags, widths and argument indexes
Compares zero padding, left alignment, string truncation and reuse of one argument in several places.
public class FormatPadding {
public static void main(String[] args) {
System.out.printf("[%05d] [%-5d] [%5d]%n", 42, 42, 42);
System.out.printf("[%.3s] [%8s] [%-8s]%n", "formatting", "abc", "abc");
System.out.printf("%1$s is %2$d chars, so %1$s fits%n", "kiwi", 4);
System.out.printf("%x %o %b %c%n", 255, 8, "yes", 65);
}
}Example explained
Line 1The 0 flag fills the field with zeros and - switches to left alignment; with neither flag, %5d right-aligns.
Line 2.3 on a string conversion cuts "formatting" down to "for" first, and only then would any width padding apply.
Line 31$ selects the first argument explicitly, so "kiwi" appears twice while only two arguments are passed.
Line 4%b prints true for any non-null argument, so on the String "yes" it reports non-nullness rather than truth.
Important notes
%n emits the platform line separator (CRLF on Windows) while \n is always one LF; use %n for console text and \n when the exact bytes matter.
Flags are validated against the conversion: %-05d throws IllegalFormatFlagsException because - and 0 contradict each other, and %,x throws FormatFlagsConversionMismatchException because grouping is meaningless in hex.
Common mistakes
Pairing %d with a double or %f with an int: the code compiles, then throws IllegalFormatConversionException the first time that line executes.
Leaving a bare % in the prose, as in "20% off": Formatter reads it as the start of a specifier and throws UnknownFormatConversionException or MissingFormatArgumentException, so a literal percent must be written %%.
Trusting width to keep a table tidy: %8s on a 12-character name prints all 12 characters and shifts every column after it, because only .precision can cap length.
Try it yourself
Change, predict, then run
Use printf to print three rows of fruit name, count and unit price with the name left-aligned in 12 characters, the count right-aligned in 4 and the price at exactly two decimals in width 8. Then build one of the rows with String.format and print its length to confirm every row is the same width.
Open the Java workspaceCheck your understanding
What does String.format("%4s|%4.2s|", "abcdef", "abcdef") produce?
- abcd| ab|
- abcdef| ab|
- abcdef|abcdef|
- abcdef|ab |
Show answer
Width is a minimum field size, so %4s prints all six characters of "abcdef" untouched. In %4.2s the precision runs first and reduces the argument to "ab", then width 4 pads it, and since no - flag is present the padding lands on the left, giving " ab". The tempting answer "abcd| ab|" assumes width clips long values, which it never does.