Number Format
Showing a number to a person involves more than converting it to text. The decimal separator, the grouping separator, the currency symbol and its position, and the number of decimal places all depend on the user's language and country. java.text.NumberFormat and its subclass DecimalFormat handle these conventions: they turn numbers into correctly formatted text for a given locale, and parse such text back into numbers.
Why Locale Matters
The same value is written differently around the world:
| Locale | Number | Currency | Percent |
|---|---|---|---|
en-US | 1,234,567.891 | $1,234,567.89 | 18% |
en-IN | 1,234,567.891 | ₹1,234,567.89 | 18% |
de-DE | 1.234.567,891 | 1.234.567,89 € | 18 % |
fr-FR | 1 234 567,891 | 1 234 567,89 € | 18 % |
ja-JP | 1,234,567.891 | ¥1,234,568 | 18% |
A German reader sees 1.234 as one thousand two hundred thirty-four, not as one point two three four. Japanese yen has no minor unit, so the currency format shows no decimals. French uses a narrow non-breaking space to group digits. Hard-coding "," and "." produces output that is wrong, and sometimes dangerously misleading, for many users.
Getting a NumberFormat
NumberFormat provides factory methods for common styles:
import java.text.NumberFormat;
import java.util.Locale;
Locale india = Locale.forLanguageTag("en-IN");
NumberFormat number = NumberFormat.getNumberInstance(india); // general numbers
NumberFormat currency = NumberFormat.getCurrencyInstance(india); // money
NumberFormat percent = NumberFormat.getPercentInstance(india); // percentages
NumberFormat integer = NumberFormat.getIntegerInstance(india); // whole numbers, rounded
currency.format(1499.5); // "₹1,499.50"
percent.format(0.175); // "18%": the value is multiplied by 100
Without a locale argument, each method uses the JVM's default locale, which varies between machines. Pass an explicit Locale for predictable output.
Locales are created with Locale.forLanguageTag("en-IN"), Locale.of("en", "IN") (Java 19+), or constants such as Locale.US and Locale.GERMANY. In a web application, the locale usually comes from the user's profile or the browser's Accept-Language header.
Controlling decimals and rounding
NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
nf.setRoundingMode(RoundingMode.HALF_UP);
nf.format(1234.5); // "1,234.50"
nf.setGroupingUsed(false);
nf.format(1234.5); // "1234.50"
The default rounding mode is HALF_EVEN. When formatting double values, rounding applies to the exact binary value, so a double written as 2.345 (actually slightly less) may round differently from what you expect. Format BigDecimal values for exact results.
Currency Formatting
NumberFormat inr = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("en-IN"));
inr.getCurrency(); // INR
inr.getMaximumFractionDigits(); // 2
NumberFormat usd = NumberFormat.getCurrencyInstance(Locale.US);
usd.format(new BigDecimal("12499.50")); // "$12,499.50"
The locale determines both the formatting conventions and the default currency. To show a specific currency in a particular locale's style, set it explicitly: fmt.setCurrency(Currency.getInstance("EUR")).
NumberFormat accepts BigDecimal and BigInteger as well as primitives. For money, store and calculate with BigDecimal and format only at the point of display.
Indian lakh and crore grouping
Indian numbering groups the last three digits and then every two digits: 23,45,678.90 (twenty-three lakh). Java's DecimalFormat supports only a single, uniform grouping size, so even with the en-IN locale (and even with the pattern #,##,##0.00), the standard JDK prints 2,345,678.90. Applications that must display lakh/crore grouping use the ICU4J library or a small helper method, as in the practical example below.
Compact Number Formatting
Dashboards and social media counters often show 12K or 3M instead of full numbers. getCompactNumberInstance (Java 12+) provides this:
NumberFormat shortFmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
shortFmt.format(1250); // "1K"
shortFmt.format(3_400_000); // "3M"
shortFmt.format(7_200_000_000L); // "7B"
NumberFormat longFmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
longFmt.format(3_400_000); // "3 million"
By default, compact formats round to whole numbers.
DecimalFormat Patterns
DecimalFormat lets you describe the format with a pattern:
import java.text.DecimalFormat;
new DecimalFormat("#,##0.00").format(1234.5); // "1,234.50"
new DecimalFormat("#,##0.00").format(0.5); // "0.50"
new DecimalFormat("000000").format(42); // "000042"
new DecimalFormat("0.##").format(2.5); // "2.5"
new DecimalFormat("0.##").format(2.0); // "2"
new DecimalFormat("0.0E0").format(123456); // "1.2E5"
new DecimalFormat("+#,##0;-#,##0").format(-1500); // "-1,500"
new DecimalFormat("#,##0.00 'kg'").format(12.5); // "12.50 kg"
| Symbol | Meaning |
|---|---|
0 | A digit, shown even if zero |
# | A digit, omitted if it is a leading or trailing zero |
, | Grouping separator position |
. | Decimal separator |
% | Multiply by 100 and show a percent sign |
E | Scientific notation |
; | Separates the positive and negative patterns |
'text' | Literal text |
The symbols in a pattern are placeholders. The characters actually printed come from the locale's DecimalFormatSymbols, so #,##0.00 produces 1.234,50 with a German locale. Use new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(locale)) to choose them explicitly.
Parsing Formatted Text
parse converts locale-formatted text back into a Number:
NumberFormat.getNumberInstance(Locale.GERMANY).parse("1.234,56"); // 1234.56
NumberFormat.getNumberInstance(Locale.US).parse("abc"); // ParseException: Unparseable number: "abc"
NumberFormat.getNumberInstance(Locale.US).parse("1,234.56abc"); // 1234.56: trailing text ignored!
parse stops at the first character it does not understand and ignores the rest. To reject input with trailing garbage, use the parse(String, ParsePosition) overload and check that the position reached the end of the string. For exact decimal results, call ((DecimalFormat) fmt).setParseBigDecimal(true).
Thread Safety
NumberFormat and DecimalFormat instances are not thread-safe: they hold internal state while formatting. Do not share one instance between threads, such as in a static field of a web controller. Create one per use, keep one per thread (ThreadLocal), or synchronize access. (String.format creates its own formatter each time and is safe to call from any thread.)
NumberFormat Versus String.format
String.format("%,.2f", x) | NumberFormat | |
|---|---|---|
| Locale-aware separators | Yes (with a Locale argument) | Yes |
| Currency symbols and placement | No | Yes |
Compact formats (12K) | No | Yes |
| Custom patterns | Limited | DecimalFormat patterns |
| Parsing | No | Yes |
| Thread-safe | Yes | No |
Use String.format for simple, fixed layouts, such as logs, reports, and machine-readable files with Locale.ROOT. Use NumberFormat for anything a user reads as money, percentages, or localized numbers.
Practical Example: Price Tags for Several Audiences
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.*;
public class PriceTags {
static String indianGrouping(BigDecimal amount) {
BigDecimal rounded = amount.setScale(2, RoundingMode.HALF_UP);
String plain = rounded.abs().toPlainString();
String whole = plain.substring(0, plain.indexOf('.'));
String fraction = plain.substring(plain.indexOf('.'));
StringBuilder sb = new StringBuilder();
int len = whole.length();
if (len <= 3) {
sb.append(whole);
} else {
String head = whole.substring(0, len - 3);
for (int i = 0; i < head.length(); i++) {
if (i > 0 && (head.length() - i) % 2 == 0) sb.append(',');
sb.append(head.charAt(i));
}
sb.append(',').append(whole.substring(len - 3));
}
return (rounded.signum() < 0 ? "-₹" : "₹") + sb + fraction;
}
public static void main(String[] args) {
List<BigDecimal> prices = List.of(new BigDecimal("499"), new BigDecimal("12499.5"),
new BigDecimal("150000"), new BigDecimal("2345678.904"), new BigDecimal("-75000"));
NumberFormat us = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat inr = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("en-IN"));
NumberFormat compact = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.printf("%-16s %-16s %-16s %s%n", "Indian grouping", "en-IN locale", "US dollars", "Compact");
for (BigDecimal p : prices) {
System.out.printf("%-16s %-16s %-16s %s%n", indianGrouping(p), inr.format(p), us.format(p), compact.format(p));
}
}
}
Output:
Indian grouping en-IN locale US dollars Compact
₹499.00 ₹499.00 $499.00 499
₹12,499.50 ₹12,499.50 $12,499.50 12K
₹1,50,000.00 ₹150,000.00 $150,000.00 150K
₹23,45,678.90 ₹2,345,678.90 $2,345,678.90 2M
-₹75,000.00 -₹75,000.00 -$75,000.00 -75K
The second and third columns come straight from NumberFormat.getCurrencyInstance, with the symbol, grouping, and two decimal places chosen by the locale. The compact column suits small UI spaces, such as a sales badge. The first column comes from indianGrouping, which fills the gap described above:
- It rounds with
BigDecimalto exactly two decimal places. - It groups the last three digits of the whole part, then every two digits before them, producing
1,50,000.00(one lakh fifty thousand) and23,45,678.90. - It handles the sign separately so that negative amounts read
-₹75,000.00.
For production systems that need lakh/crore formatting along with full localization, the ICU4J library supports Indian grouping directly.