java.text Formatting
Format strings, numbers, dates, and messages with java.text: MessageFormat, NumberFormat, DecimalFormat, and printf-style formatting.
Introduction
The java.text package provides classes and utilities for formatting and parsing dates, numbers, messages, and strings. While java.time covers date-time objects, java.text handles the broader formatting needs: localized number display, composite message templates, and printf-style positional formatting.
When to Use
| Use Case | Class |
|---|---|
| Localized number formatting | NumberFormat, DecimalFormat |
| Currency display | NumberFormat.getCurrencyInstance() |
| Percentage display | NumberFormat.getPercentInstance() |
| Date/time formatting (legacy, pre-Java 8) | DateFormat, SimpleDateFormat |
| Composite message templates with placeholders | MessageFormat |
| Printf-style positional formatting | String.format(), Formatter |
| Decimal precision control | DecimalFormat |
| Plural-aware messages | MessageFormat with ChoiceFormat |
When NOT to Use
- New date/time code: Use
java.time.DateTimeFormatter(fromjava.time.format) instead ofDateFormat/SimpleDateFormat. - JSON serialization: Use Jackson or Gson with their type adapters, not manual formatting.
- Locale-sensitive sorting: Use
Collatorfromjava.textinstead of formatting-based approaches. - Secure logging of user input: Message formatting with unvalidated input can cause format string attacks.
Class Overview
classDiagram
class Format {
<<abstract>>
+format(Object) String
+parseObject(String) Object
}
class NumberFormat {
+getInstance() NumberFormat
+getNumberInstance(Locale) NumberFormat
+getCurrencyInstance(Locale) NumberFormat
+getPercentInstance(Locale) NumberFormat
+getIntegerInstance(Locale) NumberFormat
+format(double) String
+parse(String) Number
}
class DecimalFormat {
+applyPattern(String) void
+applyLocalizedPattern(String) void
+setMinimumFractionDigits(int)
+setMaximumFractionDigits(int)
}
class MessageFormat {
+format(String, Object[]) String
+applyPattern(String) void
+setFormatByArgumentIndex(int, Format)
}
class DateFormat {
+getDateInstance(int, Locale) DateFormat
+getTimeInstance(int, Locale) DateFormat
+getDateTimeInstance(int, int, Locale) DateFormat
}
class SimpleDateFormat {
+SimpleDateFormat(String pattern)
+applyPattern(String)
}
Format <|-- NumberFormat
Format <|-- MessageFormat
Format <|-- DateFormat
NumberFormat <|-- DecimalFormat
DateFormat <|-- SimpleDateFormat
Code Examples
NumberFormat — Localized Number Formatting
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.util.Locale;
// General number formats
NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
System.out.println(nf.format(1234567.89)); // 1,234,567.89
// Integer only
NumberFormat integerNf = NumberFormat.getIntegerInstance(Locale.GERMANY);
System.out.println(integerNf.format(1234)); // 1.234
// Currency
NumberFormat cf = NumberFormat.getCurrencyInstance(Locale.US);
System.out.println(cf.format(1234.56)); // $1,234.56
// Percentage
NumberFormat pf = NumberFormat.getPercentInstance(Locale.UK);
pf.setMaximumFractionDigits(2);
System.out.println(pf.format(0.5678)); // 56.78%
// Rounding modes
NumberFormat rf = NumberFormat.getNumberInstance();
rf.setMinimumFractionDigits(2);
rf.setMaximumFractionDigits(2);
rf.setRoundingMode(java.math.RoundingMode.HALF_UP);
System.out.println(rf.format(99.999)); // 100.00
DecimalFormat — Pattern-Based Number Formatting
import java.text.DecimalFormat;
import java.math.RoundingMode;
DecimalFormat df = new DecimalFormat("###,###.##");
System.out.println(df.format(1234567.89)); // 1,234,567.89
// Force decimals
DecimalFormat df2 = new DecimalFormat("00000.000");
System.out.println(df2.format(42.5)); // 00042.500
// Scientific notation
DecimalFormat df3 = new DecimalFormat("0.###E0");
System.out.println(df3.format(1234567)); // 1.2346E6
// Currency with pattern
DecimalFormat cf = new DecimalFormat("¤#,###.00");
cf.setCurrency(java.util.Currency.getInstance("EUR"));
System.out.println(cf.format(1234.56)); // €1,234.56
// Pad with zeros
DecimalFormat padded = new DecimalFormat("000000");
System.out.println(padded.format(42)); // 000042
MessageFormat — Composite Message Templates
import java.text.MessageFormat;
import java.text.ChoiceFormat;
import java.util.Date;
import java.util.Locale;
// Simple positional arguments
String msg = MessageFormat.format(
"Hello {0}, you have {1} messages.",
"Alice", 5
);
System.out.println(msg);
// Hello Alice, you have 5 messages.
// Named arguments via numeric index with format
String formatted = MessageFormat.format(
"On {1,date,long} at {1,time,short}, {0} purchased {2,number,integer} units for {2,number,currency}.",
"Bob", new Date(), 10
);
// ChoiceFormat for plural-aware messages
double[] limits = {0, 1, 2};
String[] formats = {"no files", "one file", "{0} files"};
ChoiceFormat cf = new ChoiceFormat(limits, formats);
MessageFormat mf = new MessageFormat("You selected {0}.");
mf.setFormat(0, cf);
System.out.println(mf.format(new Object[]{0.0})); // You selected no files.
System.out.println(mf.format(new Object[]{1.0})); // You selected one file.
System.out.println(mf.format(new Object[]{5.0})); // You selected 5 files.
printf-Style Formatting with String.format
// String.format uses Formatter syntax
String s = String.format("Hello %s, day %d", "Alice", 3);
// Hello Alice, day 3
// Width and alignment
String table = String.format("%-10s %5d %8.2f", "Apple", 5, 2.5);
// Apple 5 2.50
// Hex, octal, binary
String hex = String.format("0x%02X", 255); // 0xFF
String oct = String.format("%o", 64); // 100
String bin = String.format("%08b", 42); // 00101010
// Escaping %
String pct = String.format("100%% complete"); // 100% complete
// Locale-aware formatting
String localized = String.format(Locale.GERMANY, "%,.2f", 1234567.89);
// 1.234.567,89
SimpleDateFormat — Legacy Date Formatting (Prefer java.time)
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
// Note: for new code, prefer DateTimeFormatter from java.time
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date())); // 2026-05-23 14:30:00
// Locale-specific format
SimpleDateFormat german = new SimpleDateFormat("EEEE, d. MMMM yyyy", Locale.GERMAN);
System.out.println(german.format(new Date())); // Samstag, 23. Mai 2026
// Parsing
Date parsed = sdf.parse("2026-05-23 14:30:00");
// Patterns
// yyyy = 4-digit year, MM = month, dd = day
// HH = hour (24h), mm = minutes, ss = seconds
// EEEE = day name, MMMM = month name
Failure Scenarios
| Scenario | Problem | Solution |
|---|---|---|
SimpleDateFormat in multithreaded code | Race condition due to shared mutable state | Use ThreadLocal<SimpleDateFormat> or migrate to DateTimeFormatter |
MessageFormat with mismatched argument count | IllegalArgumentException | Ensure argument array length matches placeholder indices |
DecimalFormat parsing user input | ParseException on malformed input | Wrap in try-catch or use regex pre-validation |
String.format with null argument | NullPointerException for %s | Use String.format("%s", null) or guard nulls explicitly |
| Locale mismatch | NumberFormat.getNumberInstance(Locale.FRANCE) on Locale.US JVM | Set locale explicitly in the formatter call |
Trade-off Table
| Aspect | java.text | java.time formatting |
|---|---|---|
| Thread safety | Most classes not thread-safe | DateTimeFormatter is immutable and thread-safe |
| Date handling | Date/Calendar mutable types | LocalDate, Instant, etc. immutable |
| Pattern syntax | Legacy pattern letters | ISO-8601 based patterns |
| Localization | Full i18n support | Full i18n via locale-specific formatters |
| API complexity | Verbose | Cleaner, domain-specific |
Observability Checklist
// Instrumented formatter
public class FormattedOutput {
public static String formatMetric(String name, double value, String unit) {
String formatted = String.format("%s=%.2f%s timestamp=%s",
name, value, unit, java.time.Instant.now());
System.out.println(formatted);
return formatted;
}
public static String formatLocaleSensitive(List<Double> values, Locale locale) {
NumberFormat nf = NumberFormat.getNumberInstance(locale);
return values.stream()
.map(nf::format)
.collect(Collectors.joining(", "));
}
}
- Use structured logging for all formatted output rather than concatenating format results to log strings.
- Instrument parse operations with success/failure counts.
- Track locale distribution of formatted numbers to understand user base.
- Log format pattern mismatches as WARN-level events.
- Use
%swith a label rather than anonymous%splaceholders for debuggability.
Security Notes
- Format string vulnerabilities: While less common in Java than C, passing user-controlled format strings to
String.format()orFormattercan cause unexpected behavior — especially patterns containing format specifiers that parse as%n(newline injection). Never pass raw user input as the format string. - Locale injection: A crafted
Localeobject could contain unexpected values that affect formatting behavior. Validate locale values against an allowlist. - Sensitive data in formatted output: Ensure currency-formatted values or percentage computations on financial data do not leak to structured logs accessible to unauthorized parties.
SimpleDateFormatparse lenient mode: WhensetLenient(true)is set, invalid dates like2026-02-30are silently parsed as2026-03-02instead of throwing. Use strict mode for validated inputs.
Pitfalls
SimpleDateFormatis not thread-safe: Sharing a singleSimpleDateFormatinstance across threads causes race conditions. In Java 8+, useDateTimeFormatterwhich is immutable and thread-safe.MessageFormatwith ChoiceFormat and floating point:ChoiceFormatuses exact double matching — floating-point rounding can cause the wrong choice branch. Use integers or explicit ranges.String.formatlocale defaults to JVM locale: On a German JVM,String.format("%f", 3.14)produces"3,14"which breaks JSON APIs expecting"3.14". Always passLocale.ROOTfor machine-readable output.DecimalFormatpatterns vary by locale: The,and.pattern characters represent grouping separator and decimal point respectively — not literal locale characters. The actual output is locale-dependent.MessageFormatargument indexing starts at 0:{0},{1}, etc. refer to argument positions directly — unlikeprintfwhich uses the order of arguments.
Quick Recap
NumberFormat.getNumberInstance(),getCurrencyInstance(),getPercentInstance()for localized number display.DecimalFormatfor pattern-based numeric formatting with precise control over digits and grouping.MessageFormatfor composite templates with positional{0},{1}placeholders and ChoiceFormat for plurals.String.format()for printf-style formatting with width, precision, and locale control.SimpleDateFormatis legacy (pre-Java 8) — useDateTimeFormatterfor new date/time code.- Always pass
Locale.ROOTfor machine-readable output formats (JSON, APIs). MessageFormatpatterns use{index,type,style}syntax for formatted arguments.
Interview Questions
Model Answer: "NumberFormat is the abstract base class for all number formatters and provides factory methods for common instances (getNumberInstance(), getCurrencyInstance(), getPercentInstance()). DecimalFormat is a concrete subclass that uses decimal pattern strings (like ###,###.##) to precisely control digit grouping, decimal places, leading zeros, and currency symbols. Use NumberFormat for locale-aware general needs; use DecimalFormat when you need explicit pattern control."
Model Answer: "SimpleDateFormat is not thread-safe — its internal calendar and formatting state are shared mutable fields that cause race conditions when shared across threads. In Java 8+, use java.time.format.DateTimeFormatter which is immutable and thread-safe. DateTimeFormatter uses LocalDate, LocalDateTime, Instant, etc., which are also immutable and thread-safe."
Model Answer: "MessageFormat itself does not handle pluralization directly. Instead, it pairs with ChoiceFormat (a subclass of Format) which maps numeric ranges to formatted strings. You define limits (e.g., 0, 1, 2) and corresponding formats (e.g., no files, one file, {0} files), then attach the ChoiceFormat to the argument index in MessageFormat. The system selects the appropriate format based on the argument value."
Model Answer: "In String.format patterns: 0 is the zero-padding flag (pads with zeros on the left); 8 is the minimum field width (the result must be at least 8 characters); b is the conversion type — binary (b). So %08b formats the argument as binary, zero-padded to 8 characters. For 42, this produces 00101010. Other flags: - for left alignment, , for locale-specific grouping, ( for negative numbers in parentheses."
Model Answer: "In Java, the format string itself is controlled by your code, not directly by user input — unlike C's printf. However, if a user-controlled string is used as the format string (e.g., String.format(userInput, args)), a malicious input could contain patterns like %s%n which attempts to write to memory locations via the %n specifier. In some JVM implementations, %n in a format string can cause issues. Always validate and whitelist user input that will be used in format patterns."
Model Answer: "ChoiceFormat maps numeric ranges to formatted strings using limit/format pairs. You set it on a MessageFormat via setFormat(index, choiceFormat). When the formatted value falls within a range, the corresponding format string is used. The format string can reference the argument value via {0}. For example: new ChoiceFormat(0=no files, 1=one file, {0}=files) produces different messages based on the quantity. This is limited to exact double matching — floating-point precision issues can cause wrong choices."
Model Answer: "In DecimalFormat patterns, . represents the decimal separator (locale-dependent — . in US, , in some European locales) and , represents the grouping separator (comma in US, period or space in some locales). These pattern characters produce locale-appropriate output when using format() on a DecimalFormat instance. They are not literal characters — the pattern specifies the structure, not the literal output characters."
Model Answer: "The 0 is the zero-padding flag, and 5 is the minimum field width. So %05d formats an integer with a minimum width of 5 characters, padding with zeros on the left if necessary. For example, String.format(%05d, 42) produces 00042. The 0 flag only applies when the value is shorter than the minimum width — it does not truncate. Negative numbers with 0 padding retain the minus sign."
Model Answer: "NumberFormat.getCurrencyInstance(Locale.US) formats currency using the USD symbol and US decimal/grouping rules. NumberFormat.getCurrencyInstance(Locale.GERMANY) uses EUR and German formatting rules (period as grouping separator, comma for decimal). When formatting for display to users, always use their locale. When formatting for machine consumption (APIs, storage), use Locale.ROOT."
Model Answer: "NumberFormat handles number formatting (decimal, currency, percent, integer) with locale-aware patterns. ChoiceFormat extends Format and maps numeric ranges to formatted strings. They are often used together — MessageFormat accepts ChoiceFormat via setFormat() for value-based message selection. NumberFormat does not select between format strings based on value; it only formats numbers according to a pattern."
Model Answer: "new DecimalFormat(###,###.##) constructs and applies the pattern in one step. applyPattern(String) is used on an existing DecimalFormat instance to change its pattern. Both behave identically after the pattern is applied. The choice is stylistic — use the constructor for one-off creation and applyPattern when changing the format dynamically on the same instance."
Model Answer: "ChoiceFormat selects between format strings based on numeric ranges. It is the programmatic way to implement pluralization and value-based messaging without if-else chains. For example, a file copying dialog might use ChoiceFormat to produce 0 files, 1 file, N files based on the count. It is more maintainable than if-else for many cases and integrates with MessageFormat for composite messages."
Model Answer: "The comma , flag uses locale-specific grouping separators. On a US system, String.format(%,.2f, 1234567.89) produces 1,234,567.89. On a German system, it produces 1.234.567,89 because German uses period for grouping and comma for decimal. For machine-readable output (JSON, CSV), always pass Locale.ROOT to avoid JVM locale defaults causing inconsistent formatting."
Model Answer: "DecimalFormat rounds the value according to the RoundingMode in effect (default is HALF_EVEN). For the pattern ###,###.## and value 1234.567, the result is 1,234.57 — rounding to two decimal places. Set setRoundingMode() to control rounding behavior. For truncation without rounding, use DecimalFormat with a BigDecimal that has been pre-rounded."
Model Answer: "MessageFormat placeholder syntax is {argumentIndex,type,style}. type can be number, date, time, or choice. style specifies the format variant. For example: {1,date,long} formats argument at index 1 as a date using the long date style (e.g., January 23, 2026). {0,number,integer} formats argument 0 as an integer without fractional parts."
Model Answer: "NumberFormat.parse() returns a Number object (or null for empty input). For unparseable input, it throws ParseException — the caller must handle this. Unlike format(), which never throws for valid inputs, parse() can fail on malformed strings. Always wrap parse() in a try-catch block and validate input with regex or other means before parsing if the input is from an untrusted source."
Model Answer: "String.format(String template, Object... args) is a convenience method that internally creates a Formatter, formats the arguments, and returns the result as a string. Formatter is the underlying class that accepts an Appendable (such as StringBuilder, PrintStream, or Writer) as its destination. Use Formatter directly when you want to write to an Appendable incrementally rather than producing a single formatted string."
Model Answer: "setMaximumFractionDigits(int) limits the maximum number of digits after the decimal separator. Values beyond this are rounded according to the rounding mode. setMinimumFractionDigits(int) ensures at least that many decimal places — padding with zeros if necessary. Both affect output formatting; they do not change the underlying value's precision."
Model Answer: "MessageFormat ignores extra arguments beyond what the pattern references. If a placeholder references an argument index that does not exist in the array, it outputs the placeholder text unchanged (e.g., {3} appears as-is if only 2 arguments are provided). No exception is thrown for missing arguments — the pattern is output with missing placeholders visible as literal text."
Model Answer: "DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale) returns a formatter for combined date and time, but it formats using the legacy java.util.Date and Calendar types which are mutable and not thread-safe. DateTimeFormatter (from java.time) works with the immutable LocalDateTime, ZonedDateTime, and Instant types and is fully thread-safe. Always prefer DateTimeFormatter for new code."
Further Reading
- Oracle: Formatting — official Java tutorial on
java.textformatting - Baeldung: Java NumberFormat Guide — practical coverage of
NumberFormatandDecimalFormat - ICU4J: International Components for Unicode — advanced plural handling and locale-aware message formatting beyond
ChoiceFormat - Oracle: DecimalFormat Javadoc — official reference for pattern syntax
- Stack Overflow: SimpleDateFormat thread safety — community discussion on the classic thread-safety bug and workarounds
Conclusion
java.text provides formatting for numbers, messages, and dates that the java.time package does not cover. NumberFormat and DecimalFormat handle localized number display (thousands separators, decimal points, currency symbols), MessageFormat handles composite message templates with positional placeholders, and String.format() handles printf-style positional formatting. For new date/time formatting code, always prefer java.time.DateTimeFormatter over SimpleDateFormat — the latter is legacy and not thread-safe.
NumberFormat is the abstract base class with factory methods for common formatting needs: getNumberInstance(), getCurrencyInstance(), getPercentInstance(), getIntegerInstance(). Each returns a formatter configured for the default locale, or pass a specific Locale for localized output. DecimalFormat is the concrete subclass that uses pattern strings like ###,###.## for precise control over digit grouping, decimal places, and padding — useful when you need exact control over numeric display format that goes beyond what the factory methods provide.
MessageFormat with ChoiceFormat enables plural-aware messages that adapt to the numeric value — “1 file” vs “5 files” — without external i18n libraries. The pattern syntax uses {index,type,style} where type can be number, date, time, or choice. For more sophisticated pluralization with proper language-aware rules, consider ICU4J, but for simple cases ChoiceFormat works well.
The critical caveat is thread safety. None of the java.text formatting classes (except NumberFormat with getInstance() returning a fresh instance) are thread-safe. Sharing a SimpleDateFormat or MessageFormat across threads causes race conditions. In Java 8+, prefer DateTimeFormatter (which is immutable and thread-safe) for date formatting, and create new formatter instances per call or use ThreadLocal for SimpleDateFormat when legacy interop requires it.
NumberFormat.getNumberInstance(),getCurrencyInstance(),getPercentInstance()for localized number display- Use
DecimalFormatpattern strings (###,###.##) for precise control over numeric format MessageFormatwith{index,type,style}syntax for composite templates with formatted argumentsChoiceFormatpairs withMessageFormatfor plural-aware messagesString.format()withLocale.ROOTfor machine-readable output (JSON, APIs) — avoid JVM locale defaultsSimpleDateFormatis legacy and not thread-safe — use java.time.DateTimeFormatter for all new date formatting- Never pass user-controlled strings as the format string to
String.format()orFormatter
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.