java.text Formatting
Format strings, numbers, dates, and messages with java.text: MessageFormat, NumberFormat, DecimalFormat, and printf-style 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
NumberFormat is the abstract base class for all locale-aware number formatting in java.text. Its factory methods return configured instances: getNumberInstance() gives you general decimal formatting, getCurrencyInstance() handles currency with the locale’s symbol, getPercentInstance() multiplies the value by 100 automatically, and getIntegerInstance() rounds toward zero for integer-only display. Each factory method takes an optional Locale argument. Pass the user’s locale for display, and pass Locale.ROOT for machine consumption.
NumberFormat parses strings back to numbers via parse(String), which returns a Number and throws ParseException on bad input. For formatting, setMinimumFractionDigits() and setMaximumFractionDigits() control decimal display, and setRoundingMode() sets the rounding strategy (default is HALF_EVEN). Locale-aware formatting matters in UI contexts. The same double value of 1234567.89 formats as "1,234,567.89" in US English, "1.234.567,89" in German, and "1 234 567,89" in French, due to different grouping and decimal separators.
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
DecimalFormat is the concrete subclass of NumberFormat that uses pattern strings for exact formatting control. Unlike the factory methods on NumberFormat, which produce locale-aware defaults, DecimalFormat gives you explicit control over every digit position. A pattern like "###,###.##" groups integer digits in thousands with commas, displays exactly two decimal places (rounding with the configured RoundingMode), and uses the locale’s decimal separator. A pattern like "00000.000" forces zero-padding on both integer and decimal sides, which is useful for fixed-width formats like leading-zero IDs or financial amounts.
The pattern characters have specific meanings: 0 forces a digit or zero if none exists, # allows a digit or nothing, . is the decimal separator (locale-dependent in output), , is the grouping separator, E produces scientific notation, and ¤ (Currency Sign) produces the locale’s currency symbol. You can apply patterns programmatically with applyPattern(String) or applyLocalizedPattern(String) to switch formats at runtime. For currency with explicit symbol placement, ¤#,###.00 with setCurrency(Currency.getInstance("EUR")) produces EUR-formatted output.
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
MessageFormat assembles composite messages from a template string and an array of arguments. The template uses positional placeholders like {0}, {1}, {2} that get replaced with the corresponding argument values. Unlike simple concatenation, MessageFormat handles formatting of different argument types. You can include {1,date,long} to format argument 1 as a date in long format, or {2,number,currency} to format argument 2 as currency. The MessageFormat.format(Object... args) static method is the quickest way to use it, and formatToCharacterIterator() gives you access to the formatted result with field positions for text styling.
For plural-aware messages, MessageFormat pairs with ChoiceFormat. ChoiceFormat maps numeric ranges to formatted strings via limit/format pairs. For example, 0 maps to "no files", 1 maps to "one file", and 2 maps to "{0} files". You attach a ChoiceFormat to a MessageFormat argument index with setFormat(index, choiceFormat). The result is a message that adapts its wording based on the numeric value, which is essential for grammatically correct internationalized messages. One thing to watch out for: ChoiceFormat uses exact double matching, so floating-point precision issues can cause wrong branch selection.
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(String template, Object... args) and the related Formatter class implement printf-style positional formatting in Java. The format string contains literal text plus format specifiers. Tokens like %s (string), %d (decimal integer), %f (floating point), %x (hex), %o (octal), and %b (boolean) get replaced by the corresponding argument. Flags modify the output: %-10s left-aligns a string in a 10-character field, %05d zero-pads an integer to 5 digits, %,.2f uses locale-specific grouping separators for large numbers.
The format specifier syntax is %[argument_index$][flags][width][.precision]conversion. The optional argument_index$ (e.g., %2$s) specifies which argument to use, allowing out-of-order or repeated placeholders. Width sets the minimum field width. Precision sets the maximum for strings (truncation) or decimals (decimal places). The comma flag %, enables locale-aware grouping separators. Here is the catch: String.format on a German JVM produces "1.234.567,89" for %,.2f, which breaks JSON APIs expecting "1234567.89". Always pass Locale.ROOT as the first argument when formatting for machine consumption.
// 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)
SimpleDateFormat is the legacy date formatter from pre-Java 8 that formats and parses dates using pattern strings like "yyyy-MM-dd HH:mm:ss". The pattern letters (yyyy = 4-digit year, MM = month, dd = day, HH = hour, mm = minute, ss = second, EEEE = full day name) are widely familiar because SimpleDateFormat was the only game in town for decades. It has two fundamental problems that make it the wrong choice for new code: it is not thread-safe, and it uses the mutable java.util.Date and java.util.Calendar types.
The thread-safety issue is the one that bites you in production. SimpleDateFormat maintains internal state (the Calendar and formatted text buffers) that gets modified on each call. Sharing a single instance across threads causes race conditions that produce garbage output or corrupt state. The old workaround was ThreadLocal<SimpleDateFormat>, but this is verbose and error-prone. Java 8 introduced java.time with DateTimeFormatter, which is immutable and thread-safe, works with the immutable LocalDate, LocalDateTime, Instant, and ZonedDateTime types, and uses a cleaner pattern syntax. If you are writing new code, just use DateTimeFormatter.
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
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
- String Class — String manipulation and the core text type
- java.util.Objects — utility methods for objects including string comparison
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.