Локализация ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-17

Класс 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.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.