Класс NumberFormat в Java: форматирование чисел, валюты и процентов
В первой версии этого урока был пример NumberFormat.getCurrencyInstance(new Locale("fr")).format(123.4567). Ожидался результат в евро, а на экран выводилось 123,46 ¤. Исключения не было: у локали без страны просто нет валюты, и Java подставляет общий знак денежной единицы. Ниже разберём, как такие ошибки возникают и как их избежать.
NumberFormat — это абстрактный класс из пакета java.text, который форматирует числа, денежные суммы и проценты по правилам конкретной локали и разбирает такие строки обратно в числа. Он знает, какой нужен десятичный разделитель (точка или запятая), разделитель разрядов, символ валюты и количество знаков после запятой.
Объект создаётся не через new, а фабричными методами вроде NumberFormat.getInstance(locale). Что такое локаль и как её создать, описано в уроке о классе Locale, а зачем всё это приложению — в уроке о локализации и интернационализации.
Как получить NumberFormat: getInstance и другие методы
Объект NumberFormat получают статическими фабричными методами: getInstance() для обычных чисел, getCurrencyInstance() для денег, getPercentInstance() для процентов. Каждый метод есть в двух вариантах: без аргументов (берётся локаль по умолчанию) и с параметром Locale.
| Метод | Что форматирует | Знаков после запятой по умолчанию | Пример для Locale.US |
|---|---|---|---|
getInstance(), getNumberInstance() | Обычное число (методы эквивалентны) | от 0 до 3 | 1234.5678 → 1,234.568 |
getIntegerInstance() | Целое число, дробь округляется | 0 | 1234.5678 → 1,235 |
getCurrencyInstance() | Денежная сумма с символом валюты | Зависит от валюты (у USD, EUR, RUB — 2) | 1234.5678 → $1,234.57 |
getPercentInstance() | Процент, число умножается на 100 | 0 | 0.256 → 26% |
getCompactNumberInstance(locale, style) | Короткая запись: 1K, 2M (Java 12+) | 0 | 1000 → 1K |
Пример NumberFormat в Java: одно и то же число в трёх локалях. Локаль передаётся явно, поэтому результат не зависит от настроек компьютера. Locale.of появился в Java 19, в более старых версиях используйте Locale.forLanguageTag("ru-RU").
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatExample1 {
public static void main(String[] args) {
double number = 1234567.891;
Locale[] locales = {Locale.US, Locale.of("ru", "RU"), Locale.GERMANY};
for (Locale locale : locales) {
NumberFormat numberFormat = NumberFormat.getInstance(locale);
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
NumberFormat percentFormat = NumberFormat.getPercentInstance(locale);
System.out.println(locale + ": " + numberFormat.format(number)
+ " | " + currencyFormat.format(number)
+ " | " + percentFormat.format(0.256));
}
}
} Вывод (Java 11+):
en_US: 1,234,567.891 | $1,234,567.89 | 26%
ru_RU: 1 234 567,891 | 1 234 567,89 ₽ | 26 %
de_DE: 1.234.567,891 | 1.234.567,89 € | 26 % Методы без аргумента, например NumberFormat.getInstance(), берут локаль по умолчанию, то есть настройки операционной системы. На сервере она может отличаться от локали на машине разработчика, поэтому в коде, результат которого важен, локаль лучше передавать явно.
Форматирование валюты: getCurrencyInstance
NumberFormat.getCurrencyInstance(locale) форматирует сумму с символом валюты страны из локали и нужным для этой валюты числом знаков после запятой. Валюта определяется по стране, а не по языку, поэтому для getCurrencyInstance нужна локаль со страной: Locale.FRANCE, а не Locale.FRENCH.
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
public class CurrencyExample {
public static void main(String[] args) {
double price = 123.4567;
System.out.println(NumberFormat.getCurrencyInstance(Locale.FRANCE).format(price));
System.out.println(NumberFormat.getCurrencyInstance(Locale.FRENCH).format(price));
// Формат США, но сумма в евро
NumberFormat usEuro = NumberFormat.getCurrencyInstance(Locale.US);
usEuro.setCurrency(Currency.getInstance("EUR"));
System.out.println(usEuro.format(1234.5));
}
}
// Вывод:
// 123,46 €
// 123,46 ¤
// €1,234.50 Метод setCurrency меняет только валюту, а разделители и порядок символов остаются от локали форматтера. Так показывают цену в валюте магазина пользователю с другими региональными настройками.
Знак ¤ вместо валюты
Если в выводе getCurrencyInstance появился символ ¤, значит, в локали не указана страна (Locale.FRENCH, Locale.ENGLISH, Locale.of("ru")). Java не может определить валюту и подставляет общий знак денежной единицы с кодом XXX. Исправление — локаль со страной (Locale.of("ru", "RU")) или явный setCurrency(Currency.getInstance("RUB")).
Количество цифр: setMaximumFractionDigits и другие
Сколько цифр выводить до и после десятичного разделителя, задают четыре метода: setMaximumFractionDigits, setMinimumFractionDigits, setMaximumIntegerDigits и setMinimumIntegerDigits. Минимум добавляет нули, максимум отбрасывает лишние цифры: дробную часть при этом округляет, а целую молча обрезает.
| Метод | Что делает | Неочевидный случай |
|---|---|---|
setMaximumFractionDigits(int) | Максимум цифр после разделителя, последняя округляется | Если новый максимум меньше минимума, минимум тоже уменьшается |
setMinimumFractionDigits(int) | Минимум цифр после разделителя, недостающие заполняются нулями | Если новый минимум больше максимума, максимум тоже увеличивается |
setMaximumIntegerDigits(int) | Максимум цифр в целой части | Старшие цифры отбрасываются без исключения: 123.45 → 23.45 |
setMinimumIntegerDigits(int) | Минимум цифр в целой части, недостающие заполняются ведущими нулями | Разделитель групп ставится и между нулями: 00,123 |
setGroupingUsed(boolean) | Включает или выключает разделитель разрядов | По умолчанию включён у всех форматтеров, кроме компактного |
getMaximumFractionDigits(), getMinimumFractionDigits() | Возвращают текущие настройки | У getInstance() это 3 и 0 |
Пример с методами getMaximumFractionDigits(), setMaximumFractionDigits(int digits) и setMaximumIntegerDigits(int digits):
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatExample2 {
public static void main(String[] args) {
double d = 123.45678;
NumberFormat nf = NumberFormat.getInstance(Locale.US);
System.out.println("Максимум знаков в дробной части "
+ nf.getMaximumFractionDigits() + ": " + nf.format(d));
nf.setMaximumFractionDigits(7);
System.out.println("Максимум знаков в дробной части 7: " + nf.format(d));
nf.setMaximumIntegerDigits(2);
System.out.println("Максимум знаков в целой части 2: " + nf.format(d));
}
}
// Вывод:
// Максимум знаков в дробной части 3: 123.457
// Максимум знаков в дробной части 7: 123.45678
// Максимум знаков в целой части 2: 23.45678 Последняя строка показывает, почему setMaximumIntegerDigits применяют с большой осторожностью. Число 123 превратилось в 23, а исключения не было. Для сумм, количеств и идентификаторов это почти всегда ошибка.
Пример с методами getMinimumFractionDigits(), setMinimumFractionDigits(int digits) и setMinimumIntegerDigits(int digits):
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatExample3 {
public static void main(String[] args) {
double d = 123.45678;
NumberFormat nf = NumberFormat.getInstance(Locale.US);
System.out.println("Минимум знаков в дробной части "
+ nf.getMinimumFractionDigits() + ": " + nf.format(d));
nf.setMinimumFractionDigits(7);
System.out.println("Минимум знаков в дробной части 7: " + nf.format(d));
nf.setMinimumIntegerDigits(5);
System.out.println("Минимум знаков в целой части 5: " + nf.format(d));
nf.setGroupingUsed(false);
System.out.println("Без разделителя групп: " + nf.format(d));
}
}
// Вывод:
// Минимум знаков в дробной части 0: 123.457
// Минимум знаков в дробной части 7: 123.4567800
// Минимум знаков в целой части 5: 00,123.4567800
// Без разделителя групп: 00123.4567800 Ведущие нули считаются обычными цифрами целой части, поэтому разделитель разрядов ставится и между ними. Если нужны номера вида 00123, отключите группировку через setGroupingUsed(false).
Округление в NumberFormat
По умолчанию NumberFormat округляет по правилу RoundingMode.HALF_EVEN («банковское» округление): ровно половина округляется к ближайшей чётной цифре. Поэтому 2.5 превращается в 2, а 3.5 — в 4. Изменить правило можно методом setRoundingMode.
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;
public class RoundingExample {
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);
System.out.println(nf.format(2.5) + " " + nf.format(3.5));
nf.setRoundingMode(RoundingMode.HALF_UP);
System.out.println(nf.format(2.5) + " " + nf.format(3.5));
}
}
// Вывод:
// 2 4
// 3 4 Округляется точное двоичное значение double, а не то, что написано в коде. Например, литерал 0.135 хранится как 0.13500000000000000888…, поэтому при двух знаках после запятой получится 0.14 даже в режиме HALF_EVEN. Для денег, где важна точная половина, передавайте в format значение BigDecimal.
Метод parse: из строки в число
Метод parse(String) преобразует строку в число по правилам локали форматтера и возвращает Number: Long, если результат целый и помещается в long, иначе Double. Если строка не начинается с числа, бросается проверяемое исключение java.text.ParseException.
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
public class NumberFormatExample4 {
public static void main(String[] args) throws ParseException {
NumberFormat ru = NumberFormat.getInstance(Locale.of("ru", "RU"));
System.out.println(ru.parse("1234,567"));
ru.setParseIntegerOnly(true);
System.out.println(ru.parse("1234,567"));
NumberFormat us = NumberFormat.getInstance(Locale.US);
System.out.println(us.parse("1234,567"));
System.out.println(us.parse("12abc"));
System.out.println(us.parse("abc"));
}
}
// Вывод:
// 1234.567
// 1234
// 1234567
// 12
// Exception in thread "main" java.text.ParseException: Unparseable number: "abc" Строки 2–4 вывода объясняют большинство ошибок с parse:
setParseIntegerOnly(true)останавливает разбор на десятичном разделителе, и возвращается только целая часть.- Одна и та же строка в разных локалях означает разные числа. В локали US запятая — разделитель разрядов, поэтому
"1234,567"превращается в 1234567. parse(String)читает число с начала строки и молча игнорирует хвост:"12abc"даёт 12. Исключение будет только в том случае, если число не найдено в самом начале строки. С Java 23 можно включить строгий режимsetStrict(true), тогда"12abc"тоже вызоветParseException.
Пробел в русской локали — неразрывный
В ru_RU разряды разделяет неразрывный пробел , а не обычный. Поэтому строку "1 234,5", набранную с клавиатуры, форматтер разберёт только до пробела и вернёт 1. Перед разбором пользовательского ввода заменяйте обычные пробелы: s.replace(' ', ' ').
DecimalFormat: свой шаблон числа
DecimalFormat — это подкласс NumberFormat, который форматирует число по явному шаблону вроде "#,##0.00". Обычно NumberFormat.getInstance() возвращает именно DecimalFormat, настроенный по шаблону локали. Создавать DecimalFormat вручную нужно, когда формат задан заранее (отчёт, номер документа) и не должен зависеть от языка пользователя.
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
public class DecimalFormatExample {
public static void main(String[] args) {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US);
System.out.println(new DecimalFormat("#,##0.00", symbols).format(1234.5));
System.out.println(new DecimalFormat("000.##", symbols).format(5.1));
NumberFormat compact = NumberFormat.getCompactNumberInstance(
Locale.US, NumberFormat.Style.SHORT);
compact.setMaximumFractionDigits(1);
System.out.println(compact.format(1000) + " " + compact.format(2_500_000));
}
}
// Вывод:
// 1,234.50
// 005.1
// 1K 2.5M В шаблоне 0 означает обязательную цифру, # — необязательную, , — место разделителя разрядов, . — десятичный разделитель. Какие символы реально подставятся вместо , и ., определяет объект DecimalFormatSymbols. Без него берутся символы локали по умолчанию.
Что выведет этот код: неочевидные моменты
Ниже три фрагмента, на которых чаще всего ошибаются и на собеседованиях, и в рабочем коде. Попробуйте ответить сами, прежде чем смотреть на вывод.
1. Процент от 25. Форматтер процентов умножает число на 100, поэтому передавать нужно долю, а не готовый процент:
NumberFormat percent = NumberFormat.getPercentInstance(Locale.US);
System.out.println(percent.format(25)); // Вывод: 2,500%
System.out.println(percent.format(0.25)); // Вывод: 25% 2. Сравнение с ожидаемой строкой в тесте. Строки выглядят одинаково, но не равны: в результате стоит неразрывный пробел.
String s = NumberFormat.getInstance(Locale.of("ru", "RU")).format(1234.5);
System.out.println(s); // Вывод: 1 234,5
System.out.println(s.equals("1 234,5")); // Вывод: false
System.out.println(s.replace(' ', ' ').equals("1 234,5")); // Вывод: true 3. Формат по умолчанию на другом сервере. Вызов NumberFormat.getInstance().format(0.5) выведет 0,5 на компьютере с русской локалью и 0.5 на сервере с английской. Если строка затем уходит в CSV или JSON, файл сломается только в продакшене. Для машинного формата передавайте Locale.ROOT или используйте DecimalFormat с явными DecimalFormatSymbols.
Часто задаваемые вопросы
Бросает ли NumberFormat исключение NumberFormatException?
Нет. NumberFormatException — непроверяемое исключение, которое бросают Integer.parseInt, Double.parseDouble и new BigDecimal(String), например на Integer.parseInt("12abc"). Метод NumberFormat.parse вместо него бросает проверяемое ParseException, и только когда число не найдено в начале строки. Чем отличается поведение на строке "12abc", показано в разделе «Метод parse: из строки в число».
Что выбрать: NumberFormat или String.format?
String.format(locale, "%,.2f", x) удобен для разового вывода и тоже учитывает локаль. NumberFormat нужен, когда требуется символ валюты, проценты, компактная запись, настройка округления или разбор строки обратно в число через parse. Настроенный форматтер можно сохранить в поле и применять многократно.
Можно ли использовать один NumberFormat из нескольких потоков?
Нет, NumberFormat и DecimalFormat не потокобезопасны: внутри они хранят изменяемое состояние, и при одновременном вызове format из разных потоков результат может перемешаться. Создавайте форматтер в каждом методе, храните его в ThreadLocal или синхронизируйте доступ.
Как форматировать денежные суммы в BigDecimal без потери точности?
Метод format принимает BigDecimal напрямую и округляет его по RoundingMode форматтера без промежуточного double. Для обратного разбора приведите форматтер к DecimalFormat и вызовите setParseBigDecimal(true): тогда parse вернёт BigDecimal, а не Double.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии