java.text Formatting

Format strings, numbers, dates, and messages with java.text: MessageFormat, NumberFormat, DecimalFormat, and printf-style formatting.

published: reading time: 17 min read author: Geek Workbench

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 CaseClass
Localized number formattingNumberFormat, DecimalFormat
Currency displayNumberFormat.getCurrencyInstance()
Percentage displayNumberFormat.getPercentInstance()
Date/time formatting (legacy, pre-Java 8)DateFormat, SimpleDateFormat
Composite message templates with placeholdersMessageFormat
Printf-style positional formattingString.format(), Formatter
Decimal precision controlDecimalFormat
Plural-aware messagesMessageFormat with ChoiceFormat

When NOT to Use

  • New date/time code: Use java.time.DateTimeFormatter (from java.time.format) instead of DateFormat/SimpleDateFormat.
  • JSON serialization: Use Jackson or Gson with their type adapters, not manual formatting.
  • Locale-sensitive sorting: Use Collator from java.text instead 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

ScenarioProblemSolution
SimpleDateFormat in multithreaded codeRace condition due to shared mutable stateUse ThreadLocal<SimpleDateFormat> or migrate to DateTimeFormatter
MessageFormat with mismatched argument countIllegalArgumentExceptionEnsure argument array length matches placeholder indices
DecimalFormat parsing user inputParseException on malformed inputWrap in try-catch or use regex pre-validation
String.format with null argumentNullPointerException for %sUse String.format("%s", null) or guard nulls explicitly
Locale mismatchNumberFormat.getNumberInstance(Locale.FRANCE) on Locale.US JVMSet locale explicitly in the formatter call

Trade-off Table

Aspectjava.textjava.time formatting
Thread safetyMost classes not thread-safeDateTimeFormatter is immutable and thread-safe
Date handlingDate/Calendar mutable typesLocalDate, Instant, etc. immutable
Pattern syntaxLegacy pattern lettersISO-8601 based patterns
LocalizationFull i18n supportFull i18n via locale-specific formatters
API complexityVerboseCleaner, 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 %s with a label rather than anonymous %s placeholders for debuggability.

Security Notes

  • Format string vulnerabilities: While less common in Java than C, passing user-controlled format strings to String.format() or Formatter can 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 Locale object 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.
  • SimpleDateFormat parse lenient mode: When setLenient(true) is set, invalid dates like 2026-02-30 are silently parsed as 2026-03-02 instead of throwing. Use strict mode for validated inputs.

Pitfalls

  1. SimpleDateFormat is not thread-safe: Sharing a single SimpleDateFormat instance across threads causes race conditions. In Java 8+, use DateTimeFormatter which is immutable and thread-safe.
  2. MessageFormat with ChoiceFormat and floating point: ChoiceFormat uses exact double matching — floating-point rounding can cause the wrong choice branch. Use integers or explicit ranges.
  3. String.format locale defaults to JVM locale: On a German JVM, String.format("%f", 3.14) produces "3,14" which breaks JSON APIs expecting "3.14". Always pass Locale.ROOT for machine-readable output.
  4. DecimalFormat patterns vary by locale: The , and . pattern characters represent grouping separator and decimal point respectively — not literal locale characters. The actual output is locale-dependent.
  5. MessageFormat argument indexing starts at 0: {0}, {1}, etc. refer to argument positions directly — unlike printf which uses the order of arguments.

Quick Recap

  • NumberFormat.getNumberInstance(), getCurrencyInstance(), getPercentInstance() for localized number display.
  • DecimalFormat for pattern-based numeric formatting with precise control over digits and grouping.
  • MessageFormat for composite templates with positional {0}, {1} placeholders and ChoiceFormat for plurals.
  • String.format() for printf-style formatting with width, precision, and locale control.
  • SimpleDateFormat is legacy (pre-Java 8) — use DateTimeFormatter for new date/time code.
  • Always pass Locale.ROOT for machine-readable output formats (JSON, APIs).
  • MessageFormat patterns use {index,type,style} syntax for formatted arguments.

Interview Questions

1. What is the difference between `NumberFormat` and `DecimalFormat`?

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."


2. Why is `SimpleDateFormat` considered legacy and what should you use instead?

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."


3. How does `MessageFormat` handle pluralization?

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."


4. What does the `String.format` pattern `%08b` mean?

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."


5. What is the security concern with `String.format` and user-controlled format strings?

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."


6. How does `MessageFormat` handle `ChoiceFormat` for plural-aware messages?

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."


7. What is the difference between `DecimalFormat` pattern characters `.` and `,`?

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."


8. What does the `String.format` flag `%05d` mean?

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."


9. How does `NumberFormat.getCurrencyInstance(Locale)` behave across different locales?

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."


10. What is the difference between `NumberFormat` and `ChoiceFormat` for formatting based on value ranges?

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."


11. How does `DecimalFormat.applyPattern()` differ from constructing with a pattern string?

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."


12. What is the purpose of `ChoiceFormat` and when would you use it over if-else?

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."


13. What does `String.format("%,.2f", value)` do with the comma flag?

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."


14. What is the behavior of `DecimalFormat` when the value has more decimal places than the pattern allows?

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."


15. How does `MessageFormat` parse argument placeholders like `{1,date,long}`?

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."


16. What happens when `NumberFormat.parse()` encounters unparseable input?

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."


17. What is the difference between `String.format` and `Formatter` for constructing formatted output?

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."


18. What does `NumberFormat.setMaximumFractionDigits()` control?

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."


19. How does `MessageFormat` handle missing arguments or extra arguments in the array?

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."


20. What is the difference between `DateFormat.getDateTimeInstance()` and `DateTimeFormatter` for combined date and time?

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

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 DecimalFormat pattern strings (###,###.##) for precise control over numeric format
  • MessageFormat with {index,type,style} syntax for composite templates with formatted arguments
  • ChoiceFormat pairs with MessageFormat for plural-aware messages
  • String.format() with Locale.ROOT for machine-readable output (JSON, APIs) — avoid JVM locale defaults
  • SimpleDateFormat is 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() or Formatter

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals