String Formatting in Java: String.format(), printf() and Formatter
On a laptop with German regional settings, String.format("%.2f", 0.5) returns "0,50" instead of "0.50" — and the very next Double.parseDouble(...) fails with NumberFormatException: For input string: "0,50". Surprises like this disappear once you understand how string formatting in Java actually works.
String formatting in Java means building a string from a template in which format specifiers (%d, %s, %.2f and others) are replaced with argument values. Formatted output is produced by String.format(), String.formatted() (Java 15+), System.out.printf() and the java.util.Formatter class, which powers all of them. You can format numbers, strings, characters, dates and times.
String.format(), printf() and formatted(): which one to use
If you need the result as a string, use String.format() or "template".formatted(); if you want to print it right away, use System.out.printf(); if the text is assembled in several steps or written to a file, use the Formatter class. All of them understand exactly the same template syntax.
| Method | What it returns | Since | When to use it |
|---|---|---|---|
String.format(template, args...) | A new string | Java 5 | You need the formatted string itself (Java's sprintf) |
"template".formatted(args...) | A new string | Java 15 | Same as above, but shorter; great with text blocks |
System.out.printf(...), System.out.format(...) | The same PrintStream; prints to the console | Java 5 | Formatted console output |
PrintWriter.printf(...) | The same PrintWriter; writes to a stream | Java 5 | Writing to a file or a network stream |
new Formatter(...).format(...) | The same Formatter; accumulates the result | Java 5 | Building text from several calls, writing to an Appendable or a file |
System.out is a static field of type PrintStream, which is why it has the printf() and format() methods. They are two names for the same method: printf() exists to make C programmers feel at home. Unlike print() and println(), which print a single value as is, printf() takes a template plus any number of arguments. Under the hood, every method in the table uses Formatter.
Throughout this lesson, the output in comments is shown for Locale.US. With a locale such as German or French, you will see a comma instead of the decimal point (details in the Locale section).
public class PrintfExample {
public static void main(String[] args) {
System.out.printf("Formatted string: %.2f%n", 1000.0 / 3.0);
System.out.format("%s, next year you will be %d%n", "John", 23);
String str = String.format("Price: $%.2f", 99.5);
System.out.println(str);
String greeting = "Hello, %s! You have %d new messages.".formatted("Anna", 5);
System.out.println(greeting);
}
}
// Output:
// Formatted string: 333.33
// John, next year you will be 23
// Price: $99.50
// Hello, Anna! You have 5 new messages. The formatted() method shines with text blocks (Java 15+): you write the template as is, without concatenation or \n.
String json = """
{"name": "%s", "age": %d}
""".formatted("Anna", 30);
System.out.print(json);
// Output: {"name": "Anna", "age": 30} Format specifier syntax
A format specifier is a part of the template that starts with % and describes how to print the next argument. Its general form is %[argument_index$][flags][width][.precision]conversion; only the % sign and the conversion character are required, everything else is optional.
%[argument_index$][flags][width][.precision]conversion | Part | What it sets | Example |
|---|---|---|
argument_index$ | Argument number (starting at 1), followed by $ | %2$s — the second argument |
flags | Alignment, sign, leading zeros, grouping separators | %-10s, %,d, %+d |
width | Minimum field width in characters | %8d |
.precision | Digits after the decimal point for numbers; maximum length for strings | %.2f, %.3s |
conversion | Conversion type (required) | d, s, f, x |
The argument index lets you reuse one value several times or change the order of arguments. The %<s form means "the same argument as the previous specifier":
public class ArgumentIndexExample {
public static void main(String[] args) {
System.out.printf("Hello %1$s!%n%1$s, how are you?%n"
+ "Welcome to the site %2$s%n",
"John", "www.site.com");
System.out.printf("%d in hexadecimal: %<x%n", 255);
}
}
// Output:
// Hello John!
// John, how are you?
// Welcome to the site www.site.com
// 255 in hexadecimal: ff Java format specifiers: table
The conversion character defines how the argument is interpreted: %d is an integer, %f a floating-point number, %s a string, %c a character, %b a boolean. The argument type must match the specifier, otherwise you get an IllegalFormatConversionException at runtime.
| Specifier | What it prints | Example | Result |
|---|---|---|---|
%d | Decimal integer (int, long, BigInteger…) | format("%d", 42) | 42 |
%s / %S | String representation (toString()) | format("%S", "java") | JAVA |
%f | Floating-point number in decimal notation, 6 digits after the point by default | format("%f", 3.14159) | 3.141590 |
%e / %E | Floating-point number in scientific notation | format("%e", 12345.678) | 1.234568e+04 |
%g / %G | Floating-point number: scientific notation if, after rounding, the value is less than 10-4 or at least 10precision, otherwise decimal. Precision = total number of significant digits (6 by default) | format("%g", 12345.678)format("%g", 0.00001234) | 12345.71.23400e-05 |
%a / %A | Floating-point number in hexadecimal notation | format("%a", 1.0) | 0x1.0p0 |
%x / %X | Integer in hexadecimal | format("%X", 255) | FF |
%o | Integer in octal | format("%o", 8) | 10 |
%c / %C | Character (char or an int code point) | format("%c", 65) | A |
%b / %B | Boolean: false for null, the value itself for a Boolean, true for any other object | format("%b", null) | false |
%h / %H | The argument's hash code in hexadecimal | format("%h", "hi") | d01 |
%t / %T | Date and time (with a suffix: %tY, %tF…) | format("%tY", LocalDate.of(2026, 10, 16)) | 2026 |
%n | The platform-specific line separator | format("a%nb") | a and b on separate lines |
%% | A literal % sign | format("%d%%", 50) | 50% |
Uppercase variants (%S, %X, %E, %G, %A, %B, %H, %C, %T) work like their lowercase counterparts but convert the result to upper case: %x gives ff, %X gives FF; %b gives true, %B gives TRUE. There are no uppercase forms of %d, %f and %o.
What does this code print?
System.out.printf("%b %b %b%n", "false", null, 0); Answer: true false true. The string "false" is not a Boolean but an ordinary non-null object, so %b turns it into true. The number 0 also gives true: in Java, unlike C, zero does not mean "false". Only null becomes false.
Width and precision
Width (%10s) sets the minimum number of characters in a field: a shorter value is padded with spaces on the left, or on the right with the - flag. Precision (%.2f) sets the number of digits after the decimal point for floating-point numbers, rounding with HALF_UP; for strings (%.3s) it sets the maximum length, and the rest is cut off.
public class WidthPrecisionExample {
public static void main(String[] args) {
double x = 1000.0 / 3.0;
System.out.println("Unformatted: " + x);
System.out.printf("[%.2f]%n", x);
System.out.printf("[%8.2f]%n", x);
System.out.printf("[%-8.2f]%n", x);
System.out.printf("[%10s][%-10s]%n", "Java", "Java");
System.out.printf("[%.3s]%n", "Formatter");
System.out.printf("[%3s]%n", "Formatter");
}
}
// Output:
// Unformatted: 333.3333333333333
// [333.33]
// [ 333.33]
// [333.33 ]
// [ Java][Java ]
// [For]
// [Formatter] The last line shows a non-obvious detail: width is a minimum, not a maximum. A longer value is printed in full, and your columns drift apart. To truncate a string you need precision: %-10.10s always prints exactly 10 characters.
Width and alignment are what you need to print a table in the console:
public class TableExample {
public static void main(String[] args) {
System.out.printf("%-10s%8s%n", "Item", "Price");
System.out.printf("%-10s%8.2f%n", "Bread", 45.5);
System.out.printf("%-10s%8.2f%n", "Coffee", 89.9);
}
}
// Output:
// Item Price
// Bread 45.50
// Coffee 89.90 Formatting float and double with %f
The %f, %e and %g specifiers accept float, double, their wrappers and BigDecimal; there is no separate specifier for float and no %lf for double. Integer specifiers never convert floating-point values: %d with a float argument throws IllegalFormatConversionException: d != java.lang.Float.
float price = 19.99f;
double discount = 12.5;
System.out.println(String.format("%.2f", price));
System.out.println(String.format("%.1f%%", discount));
System.out.println(String.format("%08.3f", -3.14159));
// Output:
// 19.99
// 12.5%
// -003.142 Format flags
Flags go right after % (or after the argument index) and control alignment, the sign of a number, leading zeros and grouping separators. Several flags can be combined: %,(.2f prints a negative number in parentheses and with grouping separators.
| Flag | What it does | Example | Result |
|---|---|---|---|
- | Left-justify (requires a width) | format("[%-5d]", 42) | [42 ] |
0 | Pad with zeros instead of spaces (numbers only, requires a width) | format("%05d", 42) | 00042 |
+ | Always print the sign | format("%+d", 42) | +42 |
| space | Put a space before positive numbers (to line them up with negative ones) | format("[% d]", 42) | [ 42] |
, | Locale-specific grouping separators | format("%,d", 1234567) | 1,234,567 |
( | Enclose negative numbers in parentheses | format("%(d", -42) | (42) |
# | Alternate form: 0x prefix for %x, 0 for %o | format("%#x", 255) | 0xff |
public class FlagsExample {
public static void main(String[] args) {
System.out.printf("%,.2f%n", 10000.0 / 3.0);
System.out.printf("%,(.2f%n", -10000.0 / 3.0);
System.out.printf("%09.2f%n", 10000.0 / 3.0);
}
}
// Output:
// 3,333.33
// (3,333.33)
// 003333.33 Incompatible flags throw an exception: + together with a space, or - together with 0, gives IllegalFormatFlagsException, and - or 0 without a width gives MissingFormatWidthException.
The Formatter class
The java.util.Formatter class is the engine behind formatted output in Java: String.format() and printf() create one internally. You use Formatter directly when the text is built from several format() calls or when the result should go straight into a StringBuilder, a file or a stream.
The format() method of Formatter converts its arguments according to the template and appends the result to the accumulated text, which you get back with toString(). A minimal Java Formatter example:
import java.util.Formatter;
public class FormatterExample {
public static void main(String[] args) {
Formatter formatter = new Formatter();
formatter.format("This %s is about %n%S %c", "book", "java", '8');
System.out.println(formatter);
}
}
// Output:
// This book is about
// JAVA 8 The Formatter(Appendable, Locale) constructor writes directly into a StringBuilder and sets the locale at the same time. Formatter implements Closeable, so when it writes to a file, close it with try-with-resources. All constructors and conversions are listed in the official Formatter Javadoc.
import java.util.Formatter;
import java.util.Locale;
public class FormatterBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("%-8s%6.2f%n", "Tea", 3.5);
formatter.format("%-8s%6.2f%n", "Coffee", 12.0);
System.out.print(sb);
}
}
// Output:
// Tea 3.50
// Coffee 12.00 Locale: dot or comma
Number and date formatting depends on the locale: without an explicit Locale, String.format() and printf() use the system default (Locale.getDefault(Locale.Category.FORMAT)). In German and French the decimal separator is a comma, and the grouping separator is a dot or a space, so the same program prints different strings on different machines.
import java.util.Locale;
public class LocaleExample {
public static void main(String[] args) {
double price = 1234.567;
System.out.println(String.format(Locale.US, "%,.2f", price));
System.out.println(String.format(Locale.GERMANY, "%,.2f", price));
System.out.println(String.format(Locale.FRANCE, "%,.2f", price));
System.out.println(String.format(Locale.ROOT, "%.2f", price));
System.out.println(String.format(Locale.GERMANY, "%.2f", 0.5));
}
}
// Output:
// 1,234.57
// 1.234,57
// 1 234,57
// 1234.57
// 0,50 The French line looks like it contains an ordinary space, but it does not: since Java 13 (CLDR locale data) the French grouping separator is the narrow no-break space U+202F, and older JDKs used the no-break space U+00A0. A test such as assertEquals("1 234,57", result) typed with a regular space fails, and so does parsing that string back with code that expects plain spaces.
format() and printf() have an overload that takes a Locale as the first parameter. formatted() has no such overload — it always uses the default locale. The new Locale("de", "DE") constructor is deprecated since Java 19; use the constants (Locale.GERMANY), Locale.forLanguageTag("de-DE") or Locale.of("de", "DE") (Java 19+) instead.
Rule of thumb
For data that another program will read (CSV, JSON, SQL, logs that get parsed), pass Locale.ROOT or Locale.US explicitly. Rely on the default locale only for text that a human reads.
Formatting dates and times
The %t specifier (or %T for upper case) with a suffix prints part of a date or time: %tY is the year, %tm the month, %td the day, %tB the month name, %tH:%tM hours and minutes, %tF the ISO date. The argument can be a LocalDate, LocalDateTime, Date, Calendar or long.
import java.time.LocalDate;
import java.util.Locale;
public class DateFormatExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 10, 16);
System.out.println(String.format("%tF", date));
System.out.println(String.format("%1$tm/%1$td/%1$tY", date));
System.out.println(String.format(Locale.US, "%1$tB %1$te, %1$tY", date));
}
}
// Output:
// 2026-10-16
// 10/16/2026
// October 16, 2026 Note the %1$: without the index, every %t specifier expects its own argument, and the template "%tm/%td/%tY" with a single date throws MissingFormatArgumentException. For complex date patterns, modern code usually reaches for java.time.format.DateTimeFormatter.
Common mistakes and the exceptions they throw
The compiler does not check format strings: mistakes surface only at runtime as unchecked exceptions, subclasses of IllegalFormatException (which extends IllegalArgumentException). Here are the most frequent cases and how to fix them:
| Code | What happens | How to fix it |
|---|---|---|
format("%d", 3.5) | IllegalFormatConversionException: d != java.lang.Double | Use %f for floating-point values or cast with (int) |
format("%s and %s", "A") | MissingFormatArgumentException: Format specifier '%s' | Pass all arguments or reuse one with %1$s |
format("%-05d", 7) | IllegalFormatFlagsException: Flags = '-0' | Pick one flag: %-5d or %05d |
format("%i", 7) (a habit from C) | UnknownFormatConversionException: Conversion = 'i' | Java prints integers only with %d |
format("Save 50%") | UnknownFormatConversionException: Conversion = '%' | Escape the percent sign: 50%% |
format("%s", "a", "b") | No exception, output a — the extra argument is silently ignored | Match the number of specifiers and arguments (IDEs highlight this) |
format("%d", (Integer) null) | No exception, output null | Check the value for null beforehand |
%n or \n?
Use %n in templates: it inserts the line separator of the current OS (\r\n on Windows, \n on Linux and macOS). \n always produces a single LF character — correct only when a file format or protocol requires exactly that.
DecimalFormat and MessageFormat
Besides String.format(), the java.text package offers two formatting classes: DecimalFormat formats a number by a pattern such as #,##0.00, and MessageFormat inserts arguments by number in curly braces {0}, {1} — the approach used in UI localization bundles.
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.MessageFormat;
import java.util.Locale;
public class TextFormatExample {
public static void main(String[] args) {
DecimalFormat df = new DecimalFormat("#,##0.00",
DecimalFormatSymbols.getInstance(Locale.US));
System.out.println(df.format(1234567.891));
System.out.println(MessageFormat.format("File {0} contains {1} lines", "data.txt", 1500));
System.out.println(MessageFormat.format("File {0} contains {1,number,#} lines", "data.txt", 1500));
}
}
// Output:
// 1,234,567.89
// File data.txt contains 1,500 lines
// File data.txt contains 1500 lines A MessageFormat quirk: numbers in {1} are formatted with the locale's grouping separators, so 1500 becomes 1,500. The {1,number,#} format prints the number without separators. Another one: a single quote in a MessageFormat pattern is an escape character, so an apostrophe has to be doubled — "Don''t" prints Don't.
Frequently Asked Questions
Is there a sprintf equivalent in Java?
Yes: String.format() (or "template".formatted() in Java 15+) returns the formatted string, and System.out.printf() corresponds to C's printf. The syntax is similar but not identical: there is no %i or %u, %n is the line separator, and %b accepts any object. More in the section Common mistakes and the exceptions they throw.
What is the difference between printf() and println()?
println() prints a single value and always adds a line break. printf() takes a template and any number of arguments, but adds no line break on its own — you have to put %n into the template.
Which is faster: String.format() or concatenation with +?
Concatenation is noticeably faster: every String.format() call parses the template and creates a Formatter. In loops and hot code, build strings with + or a StringBuilder, and keep String.format() for places where readability and alignment matter. All concatenation options are compared in Java String Concatenation.
How do I keep two decimal places without rounding?
%.2f always rounds with HALF_UP: String.format("%.2f", 1.999) gives 2.00. To drop the extra digits instead, use BigDecimal: new BigDecimal("1.999").setScale(2, RoundingMode.DOWN) returns 1.99.
Comments