$ sudo teach IT
МОДУЛЬ 11 · УРОК 4

Форматирование и интернационализация

Научимся красиво оформлять числа, строки, валюты и даты. Познакомимся с локалями и созданием программ, понятных людям всего мира

~40 минут Для новичков Java

Введение — зачем нужно форматирование?

Представьте, что вы работаете кассиром в магазине. Клиент покупает товары на сумму 1234.5 рублей. Как вы напишете цену на чеке? Конечно же, так: 1 234,50 ₽, а не так: 1234.5. В первом случае человек сразу видит целую и дробную часть, понимает валюту, замечает разделитель тысяч. Это и есть форматирование — превращение «сырых» данных в удобочитаемый вид.

В программировании точно такая же задача. Когда Java выводит число 3.141592653589793, это технически корректно, но неудобно. Когда программа выводит дату в формате 1750819200000 (миллисекунды), пользователь не поймёт, что это значит. Форматирование превращает эти «сырые» значения в то, что человек сразу понимает.

Помимо форматирования, в этом уроке мы разберём интернационализацию (i18n) — процесс адаптации программы под разные языки и регионы. Знаете ли вы, что в России десятичный разделитель — это запятая, а в США — точка? Что формат даты 01.02.2025 в России означает 1 февраля, а в Америке — 2 января? Если ваша программа используется в разных странах, эти различия необходимо учитывать.

Java предоставляет мощный набор инструментов для форматирования и интернационализации. Сегодня мы подробно разберём каждый из них:

  • System.out.printf — вывод отформатированной информации в консоль
  • String.format — создание отформатированных строк без вывода
  • DecimalFormat — гибкое форматирование чисел
  • NumberFormat — форматирование чисел, валют и процентов по правилам локали
  • Locale — настройка языка и региона
  • MessageFormat — интернационализация сообщений

System.out.printf — форматированный вывод в консоль

Метод printf — это аббревиатура от «print formatted» (вывести отформатированное). Он заимствован из языка C, где используется уже более 40 лет. Принцип работы прост: вы даёте Java «шаблон» строки, в котором указываете места для подстановки значений, а затем передаёте сами значения.

Представьте это как бланк документа. У вас есть шаблон с пустыми строками: «ФИО: ____, дата рождения: ____». Вы заполняете пустые строки нужной информацией. Точно так же printf подставляет значения в места, обозначенные специальными символами (спецификаторами).

Простейший пример

Давайте начнём с самого базового:

public class PrintfDemo {
    public static void main(String[] args) {
        String name = "Алексан";
        int age = 25;
        double salary = 75000.5;

        System.out.printf("Имя: %s%n", name);
        System.out.printf("Возраст: %d лет%n", age);
        System.out.printf("Зарплата: %.2f рублей%n", salary);
        System.out.printf("Сотрудник %s (%d лет) получает %.2f руб.%n", name, age, salary);
    }
}

Вывод программы:

Имя: Александр
Возраст: 25 лет
Зарплата: 75000.50 рублей
Сотрудник Александр (25 лет) получает 75000.50 руб.

Видите? Мы написали %s, %d, %.2f в строке-шаблоне, а Java подставила вместо них реальные значения. Это гораздо удобнее, чем склеивать строки через конкатенацию.

Аналогия: Представьте, что printf — это рецептурный бланк у врача. Врач пишет «Принимать по 2 таблетки 3 раза в день после еды». Вы подставляете своё имя, дозировку и название препарата. Шаблон один, а данные у каждого пациента разные.

Основные спецификаторы формата

Спецификатор формата — это комбинация символов, которая начинается с процента % и определяет, как именно значение будет отображаться. Вот основные из них:

Спецификатор Назначение Пример значения
%s Строка "Привет"
%d Целое число (decimal) 42, -7, 1000
%f Число с плавающей запятой 3.14, -0.5
%c Символ 'A', 'я', '5'
%b Логическое значение true, false
%n Перенос строки \n или \r\n
%% Символ процента literal %
%e Научная нотация 1.23e+02
%o Восьмеричное число 52
%x Шестнадцатеричное число 2a, FF

Давайте посмотрим на каждый спецификатор подробнее:

public class SpecifiersDemo {
    public static void main(String[] args) {
        String text = "Java";
        int number = 255;
        double pi = 3.14159;
        char grade = 'A';
        boolean isGood = true;

        System.out.printf("%%s — строка: %s%n", text);
        System.out.printf("%%d — целое число: %d%n", number);
        System.out.printf("%%f — дробное число: %f%n", pi);
        System.out.printf("%%e — научная нотация: %e%n", pi);
        System.out.printf("%%c — символ: %c%n", grade);
        System.out.printf("%%b — логическое: %b%n", isGood);
        System.out.printf("%%o — восьмеричное: %o%n", number);
        System.out.printf("%%x — шестнадцатеричное: %x%n", number);

        System.out.printf("255%% = 255%%n");
    }
}

Вывод:

%s — строка: Java
%d — целое число: 255
%f — дробное число: 3.141590
%e — научная нотация: 3.141590e+00
%c — символ: A
%b — логическое: true
%o — восьмеричное: 377
%x — шестнадцатеричное: ff
255% = 255%

Почему %n, а не \n? Символ \n добавляет именно символ переноса строки Linux. Но на Windows перенос строки — это \r\n (два символа). Спецификатор %n автоматически подставляет правильный перенос для текущей операционной системы. Всегда используйте %n в printf вместо \n — это сделает вашу программу кроссплатформенной.

Ширина поля (width)

Ширина поля определяет минимальное количество символов, которое будет отведено для значения. Если значение короче — оно дополнится пробелами. Это очень удобно для создания табличных отчётов.

Аналогия: представьте таблицу в Excel. Каждая колонка имеет определённую ширину. Данные центрируются или выравниваются по левому/правому краю внутри ячейки. Спецификатор ширины делает то же самое.

public class WidthDemo {
    public static void main(String[] args) {
        System.out.printf("1234567890%n");
        System.out.printf("[%10s]%n", "Java");
        System.out.printf("[%-10s]%n", "Java");
        System.out.printf("[%20s]%n", "Привет мир");

        System.out.printf("Целое число с шириной 10: [%10d]%n", 42);
        System.out.printf("Целое число с шириной 10: [%-10d]%n", 42);

        System.out.printf("Число с шириной 15: [%15.2f]%n", 1234.5);
        System.out.printf("Число с шириной 15: [%-15.2f]%n", 1234.5);
    }
}

Вывод:

1234567890
[      Java]
[Java      ]
[         Привет мир]
Целое число с шириной 10: [        42]
Целое число с шириной 10: [42        ]
Число с шириной 15: [        1234.50]
Чисро с шириной 15: [1234.50        ]

Как видите:

  • %10s — строка выравнивается по правому краю и занимает минимум 10 символов
  • %-10s — минус перед числом выравнивает по левому краю
  • %10d — целое число с шириной 10, дополненное пробелами слева
  • %-10d — целое число выровнено по левому краю

Точность (precision)

Точность определяет количество знаков после запятой для чисел с плавающей точкой. Для строк — максимальную длину вывода.

public class PrecisionDemo {
    public static void main(String[] args) {
        double price = 1234.56789;

        System.out.printf("Без ограничений: %f%n", price);
        System.out.printf("2 знака: %.2f%n", price);
        System.out.printf("0 знаков: %.0f%n", price);
        System.out.printf("6 знаков: %.6f%n", price);

        String longText = "Очень длинная строка текста";
        System.out.printf("Строка до 10 символов: %.10s%n", longText);

        System.out.printf("Ширина 20, точность 3: [%20.3f]%n", price);
    }
}

Вывод:

Без ограничений: 1234.567890
2 знака: 1234.57
0 знаков: 1235
6 знаков: 1234.567890
Строка до 10 символов: Очень длинн
Ширина 20, точность 3: [            1234.568]

Обратите внимание: при точности .0 число 1234.56789 округлилось до 1235. Java выполняет математическое округление (а не просто отбрасывание дробной части).

Флаги выравнивания и другие флаги

Помимо ширины и точности, в спецификаторе можно использовать флаги:

  • - — выравнивание по левому краю (по умолчанию — по правому)
  • + —强制显示 знак + для положительных чисел
  • 0 — дополнение нулями вместо пробелов
  • , — разделитель групп (тысячи, миллионы)
  • ( — отрицательные числа в скобках
public class FlagsDemo {
    public static void main(String[] args) {
        int positive = 42;
        int negative = -42;
        double big = 1234567.89;

        System.out.printf("Плюс: %+d%n", positive);
        System.out.printf("Плюс отрицательное: %+d%n", negative);

        System.out.printf("Нулями: %010d%n", positive);

        System.out.printf("С разделителем: %,.2f%n", big);
        System.out.printf("С разделителем целое: %,d%n", 9876543);

        System.out.printf("Отрицательное в скобках: %(.2f%n", -1234.56);

        System.out.printf("Всё вместе: %+020,.2f%n", big);
    }
}

Вывод:

Плюс: +42
Плюс отрицательное: -42
Нулями: 0000000042
С разделителем: 1,234,567.89
С разделителем целое: 9,876,543
Отрицательное в скобках: (1234.56)
Всё вместе: +0000001,234,567.89

Флаг , особенно важен — он делает большие числа читаемыми. Вместо 1234567.89 вы видите 1,234,567.89. Это стандартный формат, который используется в финансовых отчётах по всему миру.

Порядок аргументов

Вы можете指定овать, какой аргумент подставить в какое место, используя индексы:

public class IndexDemo {
    public static void main(String[] args) {
        System.out.printf("Первый: %1$s, Второй: %2$s, Снова первый: %1$s%n",
                "Москва", "Питер");

        System.out.printf("%1$d + %2$d = %3$d%n", 10, 20, 30);
        System.out.printf("%2$d минус %1$d = %4$d%n", 10, 20, 30, 10);

        System.out.printf("Поля: [%1$10s] [%2$-8s] [%1$20s]%n", "Java", "OK");
    }
}

Вывод:

Первый: Москва, Второй: Питер, Снова первый: Москва
10 + 20 = 30
20 минус 10 = 10
Поля: [      Java] [OK      ] [                Java]

Индексы начинаются с 1 (не с 0, как в массивах). Это удобно для переиспользования одного значения в разных местах шаблона, например при создании шапки отчёта.

Аргументы формата — что ещё можно подставить

Помимо примитивных типов, printf умеет работать с объектами. Любой объект, у которого переопределён метод toString(), может быть подставлен через %s:

import java.time.LocalDate;

public class ObjectsPrintf {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        int[] numbers = {1, 2, 3};

        System.out.printf("Сегодня: %s%n", today);
        System.out.printf("Массив: %s%n", numbers);
        System.out.printf("null: %s%n", (Object) null);
        System.out.printf("Символ: %c%n", (char) 65);
    }
}

Вывод:

Сегодня: 2026-06-25
Массив: [I@1b6d3586
null: null
Символ: A

Важно: Массивы выводятся как [I@1b6d3586 — это внутреннее представление объекта в Java. Если хотите красивый вывод массива, используйте Arrays.toString() или метод Arrays.deepToString().

Практический пример: таблица сотрудников

Давайте создадим красиво оформленную таблицу сотрудников:

public class EmployeeTable {
    public static void main(String[] args) {
        String[][] employees = {
            {"Иванов И.И.", "Программист", "95000.50", "5"},
            {"Петров П.П.", "Дизайнер", "78000.00", "3"},
            {"Сидоров С.С.", "Менеджер", "85000.75", "7"},
            {"Козлова А.В.", "Аналитик", "92000.25", "4"}
        };

        String header = "| %-22s | %-16s | %14s | %6s |%n";
        String line   = "+------------------------+------------------+----------------+--------+%n";

        System.out.printf(line);
        System.out.printf(header, "ФИО", "Должность", "Зарплата", "Стаж");
        System.out.printf(line);

        for (String[] emp : employees) {
            System.out.printf("| %-22s | %-16s | %14s | %6s |%n",
                    emp[0], emp[1], emp[2], emp[3]);
        }

        System.out.printf(line);
    }
}

Вывод:

+------------------------+------------------+----------------+--------+
| ФИО                    | Должность        |        Зарплата |  Стаж |
+------------------------+------------------+----------------+--------+
| Иванов И.И.            | Программист      |        95000.50 |      5 |
| Петров П.П.            | Дизайнер         |        78000.00 |      3 |
| Сидоров С.С.           | Менеджер         |        85000.75 |      7 |
| Козлова А.В.           | Аналитик         |        92000.25 |      4 |
+------------------------+------------------+----------------+--------+

Совет: Обратите внимание на шаблон %-22s — минус означает выравнивание по левому краю. Без минуса строка выровнялась бы по правому, и таблица выглядела бы некрасиво. Всегда проверяйте выравнивание, когда создаёте табличные отчёты.

String.format — форматирование без вывода

Метод String.format() делает ровно то же самое, что и System.out.printf(), но вместо вывода в консоль возвращает отформатированную строку. Это非常 удобно, когда вы хотите сохранить результат в переменную, записать в файл или передать другому методу.

Аналогия: printf — как печатник, который сразу печатает документ на принтере. String.format — как словесный редактор, который возвращает вам готовый текст, а вы сами решаете, куда его деть.

Базовое использование

public class StringFormatDemo {
    public static void main(String[] args) {
        String name = "Александр";
        int age = 25;
        double gpa = 4.85;

        String greeting = String.format("Привет, %s! Вам %d лет.", name, age);
        System.out.println(greeting);

        String record = String.format("Студент: %s, Возраст: %d, Средний балл: %.2f",
                name, age, gpa);
        System.out.println(record);

        String padded = String.format("[%15s]", name);
        System.out.println("Выровненная строка: " + padded);

        String withIndex = String.format("%2$s (%1$s)", "RU", "Россия");
        System.out.println(withIndex);
    }
}

Вывод:

Привет, Александр! Вам 25 лет.
Студент: Александр, Возраст: 25, Средний балл: 4.85
Выровненная строка: [       Александр]
Россия (RU)

Когда использовать String.format вместо printf

Используйте String.format() когда:

  • Вы хотите сохранить результат в переменную
  • Вы строите SQL-запрос или HTTP-параметры
  • Вы создаёте лог-сообщения
  • Вы формируете сообщения для исключений
  • Вы хотите передать отформатированную строку в метод

Примеры типичного использования:

public class StringFormatUseCases {
    public static void main(String[] args) {
        int productId = 12345;
        double price = 1500.75;

        String errorMessage = String.format(
                "Ошибка в строке %d: неожиданный символ '%c'",
                42, 'x');
        System.out.println(errorMessage);

        String logMessage = String.format(
                "[ERROR] %s — Product %d, price %.2f",
                java.time.LocalDateTime.now(), productId, price);
        System.out.println(logMessage);

        String fileName = String.format("report_%04d_%02d_%02d.txt",
                2025, 6, 25);
        System.out.println("Имя файла: " + fileName);

        String sql = String.format(
                "SELECT * FROM products WHERE id = %d AND price < %.2f",
                productId, price);
        System.out.println("SQL: " + sql);
    }
}

Вывод:

Ошибка в строке 42: неожиданный символ 'x'
[ERROR] 2026-06-25T12:00:00.000 — Product 12345, price 1500.75
Имя файла: report_2025_06_25.txt
SQL: SELECT * FROM products WHERE id = 12345 AND price < 1500.75

Аналогия: String.format() — как шаблон письма в Word. Вы вставляете переменные в заготовку, и получаете готовый текст. Этот текст можно отправить по электронной почте, сохранить в файл или распечатать — вы сами решаете.

Конкатенация vs String.format — что выбрать?

Давайте сравним три способа создания строки:

public class Comparison {
    public static void main(String[] args) {
        String name = "Иван";
        int age = 30;

        String s1 = "Привет, " + name + "! Возраст: " + age;
        String s2 = String.format("Привет, %s! Возраст: %d", name, age);
        String s3 = new StringBuilder()
                .append("Привет, ")
                .append(name)
                .append("! Возраст: ")
                .append(age)
                .toString();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);

        System.out.println("Все одинаковые: " + s1.equals(s2) + " " + s2.equals(s3));
    }
}

Все три способа дают одинаковый результат. Когда что использовать:

  • Конкатенация (+) — для простых случаев, 2-3 переменных. Самый читаемый способ.
  • String.format() — когда нужное форматирование (точность, ширина, выравнивание). Идеален для отчётов и логов.
  • StringBuilder — для сложных конструкций, циклов, когда создаётся много строк. Самый быстрый способ.

DecimalFormat — гибкое форматирование чисел

Класс DecimalFormat из пакета java.text предоставляет гораздо более гибкие возможности форматирования чисел, чем printf. С его помощью вы можете создавать собственные шаблоны формата: определять, сколько знаков после запятой, как группировать цифры, использовать символы валюты и многое другое.

Аналогия: printf — как стандартные настройки камеры (автоматический режим). DecimalFormat — как ручной режим, где вы сами настраиваете диафрагму, выдержку и ISO.

Основы использования

import java.text.DecimalFormat;

public class DecimalFormatBasic {
    public static void main(String[] args) {
        double number = 1234567.891;

        DecimalFormat df1 = new DecimalFormat("#,###.##");
        System.out.println("#,###.##  : " + df1.format(number));

        DecimalFormat df2 = new DecimalFormat("#,###.00");
        System.out.println("#,###.00  : " + df2.format(number));

        DecimalFormat df3 = new DecimalFormat("###,###.0000");
        System.out.println("###,###.0000: " + df3.format(number));

        DecimalFormat df4 = new DecimalFormat("00000000.00");
        System.out.println("00000000.00: " + df4.format(number));

        DecimalFormat df5 = new DecimalFormat("#.###E0");
        System.out.println("#.###E0   : " + df5.format(number));
    }
}

Вывод:

#,###.##  : 1,234,567.89
#,###.00  : 1,234,567.89
###,###.0000: 1,234,567.8910
00000000.00: 01234567.89
#.###E0   : 1.235E6

Запомните разницу между символами # и 0:

  • # — показывает цифру только если она есть. Если цифра ведущая и равна нулю, она не отображается.
  • 0 — всегда показывает цифру. Если её нет, подставляет ноль. Идеально для выравнивания чисел по количеству цифр.

Паттерны формата — подробное разбор

Символ Описание Пример паттерна
0 Цифра (обязательная, дополняется нулями) 000.00 → 042.50
# Цифра (опциональная) ##.## → 42.5
. Десятичный разделитель #.# → 3.1
, Разделитель групп (тысячи) #,### → 1,000
E Научная нотация #E0 → 1E6
% Умножает на 100 и добавляет % ##.##% → 85.5%
‰ Умножает на 1000 и добавляет ‰ #‰ → 855‰

Форматирование валюты

import java.text.DecimalFormat;

public class CurrencyFormat {
    public static void main(String[] args) {
        double price = 1234.56;

        DecimalFormat rubleFormat = new DecimalFormat("#,##0.00 ¤");
        System.out.println("Рубли: " + rubleFormat.format(price));

        DecimalFormat dollarFormat = new DecimalFormat("$#,##0.00");
        System.out.println("Доллары: " + dollarFormat.format(price));

        DecimalFormat euroFormat = new DecimalFormat("#,##0.00 €");
        System.out.println("Евро: " + euroFormat.format(price));

        DecimalFormat simpleFormat = new DecimalFormat("#,##0.00");
        System.out.println("Без символа: " + simpleFormat.format(price));

        DecimalFormat priceTag = new DecimalFormat("#,##0 руб.");
        System.out.println("Ценник: " + priceTag.format(price));
    }
}

Вывод:

Рубли: 1,234.56 ¤
Доллары: $1,234.56
Евро: 1,234.56 €
Без символа: 1,234.56
Ценник: 1,234 руб.

Символ ¤ (валютный знак) — это универсальный символ валюты. Java автоматически заменит его на символ текущей локали. Но вы можете вписать свой символ вручную — как мы сделали с руб., $ и €.

Форматирование процентов

import java.text.DecimalFormat;

public class PercentFormat {
    public static void main(String[] args) {
        double rate = 0.125;
        double discount = 0.073;
        double tax = 0.20;

        DecimalFormat pct1 = new DecimalFormat("##.##%");
        DecimalFormat pct2 = new DecimalFormat("##.0%");
        DecimalFormat pct3 = new DecimalFormat("0.00%");
        DecimalFormat pct4 = new DecimalFormat("#%");

        System.out.println("Ставка:     " + pct1.format(rate));
        System.out.println("Скидка:     " + pct2.format(discount));
        System.out.println("Налог:      " + pct3.format(tax));
        System.out.println("Просто %:  " + pct4.format(rate));

        double value = 0.8567;
        System.out.println("\nТочность процента:");
        System.out.println("0.0%     : " + new DecimalFormat("0.0%").format(value));
        System.out.println("0.00%    : " + new DecimalFormat("0.00%").format(value));
        System.out.println("0.000%   : " + new DecimalFormat("0.000%").format(value));
    }
}

Вывод:

Ставка:     12.5%
Скидка:     7.3%
Налог:      20.00%
Просто %:  13%

Точность процента:
0.0%     : 85.7%
0.00%    : 85.67%
0.000%   : 85.670%

Важно: Символ % в паттерне автоматически умножает число на 100. Поэтому 0.125 отображается как 12.5%, а не как 0.125%. Это математически корректно: 12.5% = 0.125.

Специальные символы в паттернах

import java.text.DecimalFormat;

public class SpecialSymbols {
    public static void main(String[] args) {
        double negative = -1234.56;

        DecimalFormat df1 = new DecimalFormat("#,##0.00;(#,##0.00)");
        System.out.println("Отрицательное в скобках: " + df1.format(negative));

        DecimalFormat df2 = new DecimalFormat("+#,##0.00;-#,##0.00");
        System.out.println("С знаком: " + df2.format(negative));
        System.out.println("С знаком: " + df2.format(1234.56));

        DecimalFormat df3 = new DecimalFormat("#,##0.00 'руб.'");
        System.out.println("С текстом: " + df3.format(1234.56));

        DecimalFormat df4 = new DecimalFormat("'Цена: ' #,##0.00");
        System.out.println("С префиксом: " + df4.format(1234.56));

        DecimalFormat df5 = new DecimalFormat("#,##0.00;(#,##0.00);'нет данных'");
        System.out.println("Положительное: " + df5.format(1234.56));
        System.out.println("Отрицательное: " + df5.format(-1234.56));
        System.out.println("Ноль: " + df5.format(0));
    }
}

Вывод:

Отрицательное в скобках: (1,234.56)
С знаком: -1,234.56
С знаком: +1,234.56
С текстом: 1,234.56 руб.
С префиксом: Цена:  1,234.56
Положительное: 1,234.56
Отрицательное: (1,234.56)
Ноль: нет данных

Формат положительное;отрицательное;ноль позволяет задать три разных шаблона для разных случаев. Это非常 удобно для финансовых приложений, где отрицательные числа often показываются в скобках.

Разбор ошибок при форматировании

Типичные ошибки начинающих:

import java.text.DecimalFormat;
import java.text.ParseException;

public class FormatMistakes {
    public static void main(String[] args) {
        double num = 123.456;

        System.out.println("Правильно: " + new DecimalFormat("#.##").format(num));
        System.out.println("Правильно: " + new DecimalFormat("#.00").format(num));

        System.out.println("Два разделителя: " + new DecimalFormat("#.#.#").format(num));

        DecimalFormat wrongPattern = new DecimalFormat("abc");
        try {
            wrongPattern.parse("123");
        } catch (ParseException e) {
            System.out.println("Невозможно распарсить с этим паттерном");
        }

        DecimalFormat df = new DecimalFormat("#.##");
        System.out.println("Без разделителя групп: " + df.format(1234567.89));
        System.out.println("С разделителем: " + new DecimalFormat("#,###.##").format(1234567.89));
    }
}

Типичные ошибки:

  • Два десятичных разделителя #.#.# — некорректный паттерн
  • Забыть разделитель тысяч для больших чисел — читаемость ухудшается
  • Использовать # вместо 0 когда нужно гарантировать количество цифр
  • Не учитывать, что % умножает число на 100

NumberFormat — форматирование по правилам локали

Класс NumberFormat — это ещё один инструмент для форматирования чисел, но с важным отличием: он автоматически учитывает правила локали. В разных странах разные правила записи чисел: запятая или точка как десятичный разделитель, пробел или запятая как разделитель тысяч, символ валюты в начале или в конце.

Аналогия: DecimalFormat — как калькулятор с ручной настройкой. NumberFormat — как калькулятор, который сам определяет, в какой стране вы находитесь, и подставляет нужные настройки.

Получение экземпляра NumberFormat

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatBasic {
    public static void main(String[] args) {
        double number = 1234567.891;

        NumberFormat defaultFormat = NumberFormat.getInstance();
        System.out.println("По умолчанию: " + defaultFormat.format(number));

        NumberFormat russianFormat = NumberFormat.getInstance(new Locale("ru", "RU"));
        System.out.println("Русский: " + russianFormat.format(number));

        NumberFormat usFormat = NumberFormat.getInstance(Locale.US);
        System.out.println("Американский: " + usFormat.format(number));

        NumberFormat germanFormat = NumberFormat.getInstance(Locale.GERMANY);
        System.out.println("Немецкий: " + germanFormat.format(number));

        NumberFormat frenchFormat = NumberFormat.getInstance(Locale.FRANCE);
        System.out.println("Французский: " + frenchFormat.format(number));
    }
}

Вывод:

По умолчанию: 1,234,567.891
Русский: 1 234 567,891
Американский: 1,234,567.891
Немецкий: 1.234.567,891
Французский: 1 234 567,891

Посмотрите, как одно и то же число форматируется по-разному в зависимости от локали:

  • Русский: пробелы как разделитель тысяч, запятая как десятичный разделитель
  • Американский: запятые как разделитель тысяч, точка как десятичный разделитель
  • Немецкий: точки как разделитель тысяч, запятая как десятичный разделитель
  • Французский: пробелы как разделитель тысяч, запятая как десятичный разделитель

Почему это важно? Представьте, что вы пишете онлайн-магазин. Если клиент из России видит цену 1 234,50 ₽, а клиент из Германии видит 1.234,50 € — каждый понимает цену. Но если показать русскому клиенту 1,234.50, он может решить, что это примерно 1 тысяча 234 рубля и 50 копеек, а не 1 тысяча 234 тысячи. Именно для этого существует интернационализация.

Форматирование валюты

import java.text.NumberFormat;
import java.util.Locale;

public class CurrencyFormatting {
    public static void main(String[] args) {
        double amount = 12345.67;

        NumberFormat rubleFormat = NumberFormat.getCurrencyInstance(new Locale("ru", "RU"));
        System.out.println("Рубли: " + rubleFormat.format(amount));

        NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(Locale.US);
        System.out.println("Доллары: " + dollarFormat.format(amount));

        NumberFormat euroFormat = NumberFormat.getCurrencyInstance(Locale.GERMANY);
        System.out.println("Евро (Германия): " + euroFormat.format(amount));

        NumberFormat yenFormat = NumberFormat.getCurrencyInstance(Locale.JAPAN);
        System.out.println("Йены (Япония): " + yenFormat.format(amount));

        NumberFormat realFormat = NumberFormat.getCurrencyInstance(new Locale("pt", "BR"));
        System.out.println("Реал (Бразилия): " + realFormat.format(amount));
    }
}

Вывод (может отличаться в зависимости от JDK):

Рубли: 12 345,67 ₽
Доллары: $12,345.67
Евро (Германия): 12.345,67 €
Йены (Япония): ¥12,346
Реал (Бразилия): R$ 12.345,67

Обратите внимание на японские йены — у них нет дробной части, поэтому число округляется до целого. Это соответствует реальной практике: в Японии копейки не используются в повседневных расчётах.

Форматирование процентов

import java.text.NumberFormat;
import java.util.Locale;

public class PercentFormatting {
    public static void main(String[] args) {
        double rate = 0.1256;
        double discount = 0.073;

        NumberFormat rusPercent = NumberFormat.getPercentInstance(new Locale("ru", "RU"));
        System.out.println("Русский процент: " + rusPercent.format(rate));

        NumberFormat usPercent = NumberFormat.getPercentInstance(Locale.US);
        System.out.println("Американский процент: " + usPercent.format(rate));

        NumberFormat rusPercent2 = NumberFormat.getPercentInstance(new Locale("ru", "RU"));
        rusPercent2.setMinimumFractionDigits(2);
        System.out.println("Русский с 2 знаками: " + rusPercent2.format(rate));

        NumberFormat usPercent2 = NumberFormat.getPercentInstance(Locale.US);
        usPercent2.setMinimumFractionDigits(2);
        System.out.println("Американский с 2 знаками: " + usPercent2.format(rate));

        System.out.println("\nСкидка в России: " + rusPercent.format(discount));
        System.out.println("Скидка в США: " + usPercent.format(discount));

        rusPercent2.setMinimumFractionDigits(1);
        System.out.println("Скидка (точнее): " + rusPercent2.format(discount));
    }
}

Вывод:

Русский процент: 13%
Американский процент: 13%
Русский с 2 знаками: 12,56%
Американский с 2 знаками: 12.56%

Скидка в России: 7%
Скидка в США: 7%
Скидка (точнее): 7,3%

Метод setMinimumFractionDigits() позволяет задать минимальное количество знаков после запятой. Это非常 удобно, когда вам нужна определённая точность отображения.

Настройка NumberFormat

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatCustom {
    public static void main(String[] args) {
        double number = 1234.5;

        NumberFormat nf = NumberFormat.getInstance(Locale.US);
        nf.setMinimumIntegerDigits(6);
        System.out.println("Минимум 6 целых цифр: " + nf.format(number));

        nf.setMinimumFractionDigits(3);
        nf.setMaximumFractionDigits(3);
        System.out.println("Ровно 3 знака: " + nf.format(number));

        nf.setGroupingUsed(false);
        System.out.println("Без разделителя групп: " + nf.format(number));

        nf.setGroupingUsed(true);
        nf.setMaximumIntegerDigits(4);
        System.out.println("Максимум 4 целых цифры: " + nf.format(number));

        NumberFormat rf = NumberFormat.getInstance(new Locale("ru", "RU"));
        rf.setMinimumIntegerDigits(1);
        rf.setMinimumFractionDigits(2);
        System.out.println("\nРусский формат: " + rf.format(42.1));
        System.out.println("Русский формат: " + rf.format(1000000.5));
    }
}

Вывод:

Минимум 6 целых цифр: 001,234.5
Ровно 3 знака: 001,234.500
Без разделителя групп: 001234.500
Максимум 4 целых цифры: 1,234.500

Русский формат: 42,10
Русский формат: 1 000 000,50

Парсинг (обратное преобразование)

NumberFormat不仅能格式ировать, но и распознавать числа из строк:

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class NumberParsing {
    public static void main(String[] args) {
        NumberFormat russianFormat = NumberFormat.getInstance(new Locale("ru", "RU"));
        NumberFormat usFormat = NumberFormat.getInstance(Locale.US);

        try {
            Number num1 = russianFormat.parse("1 234,56");
            System.out.println("Русский парсинг: " + num1.doubleValue());

            Number num2 = usFormat.parse("1,234.56");
            System.out.println("Американский парсинг: " + num2.doubleValue());

            Number num3 = russianFormat.parse("1 000 000");
            System.out.println("Миллион: " + num3.doubleValue());

            Number num4 = usFormat.parse("1,000,000.00");
            System.out.println("Миллион (US): " + num4.doubleValue());
        } catch (ParseException e) {
            System.out.println("Ошибка парсинга: " + e.getMessage());
        }
    }
}

Вывод:

Русский парсинг: 1234.56
Американский парсинг: 1234.56
Миллион: 1000000.0
Миллион (US): 1000000.0

Важно: При парсинге обязательно используйте тот же экземпляр NumberFormat, которым вы форматировали число. Если вы форматировали русским форматом, парсите русским. Иначе формат не распознает разделители.

Locale — локаль и интернационализация

Locale — это объект, который определяет язык, регион и культурные особенности. Это кирпичик интернационализации. Locale влияет на всё: форматирование чисел, дат, валют, порядок сортировки строк, даже направление текста.

Аналогия: Locale — как паспорт для вашей программы. Паспорт говорит миру, откуда вы и какие правила для вас применимы. Когда ваша программа «предъявляет паспорт» с локалью ru_RU, все форматирование автоматически подстраивается под российские стандарты.

Создание Locale

import java.util.Locale;

public class LocaleCreation {
    public static void main(String[] args) {
        Locale ruLocale = new Locale("ru", "RU");
        System.out.println("Русский: " + ruLocale);
        System.out.println("  Язык: " + ruLocale.getLanguage());
        System.out.println("  Страна: " + ruLocale.getCountry());
        System.out.println("  Название: " + ruLocale.getDisplayName());
        System.out.println("  Название на русском: " + ruLocale.getDisplayName(ruLocale));
        System.out.println("  Название на английском: " + ruLocale.getDisplayName(Locale.ENGLISH));

        Locale usLocale = Locale.US;
        System.out.println("\nАмериканский: " + usLocale);
        System.out.println("  Язык: " + usLocale.getLanguage());
        System.out.println("  Страна: " + usLocale.getCountry());

        Locale germanLocale = Locale.GERMANY;
        System.out.println("\nНемецкий: " + germanLocale);

        Locale frenchLocale = new Locale("fr", "FR");
        System.out.println("\nФранцузский: " + frenchLocale);

        Locale japanLocale = Locale.JAPAN;
        System.out.println("\nЯпонский: " + japanLocale);

        Locale chinaLocale = Locale.CHINA;
        System.out.println("\nКитайский: " + chinaLocale);

        Locale britishLocale = Locale.UK;
        System.out.println("\nБританский: " + britishLocale);
    }
}

Вывод:

Русский: ru_RU
  Язык: ru
  Страна: RU
  Название: Russian (Russia)
  Название на русском: русский (Россия)
  Название на английском: Russian (Russia)

Американский: en_US
  Язык: en
  Страна: US

Немецкий: de_DE

Французский: fr_FR

Японский: ja_JP

Китайский: zh_CN

Британский: en_GB

Структура кода Locale: языок_СТРАНА. Язык — это двухбуквенный код ISO 639 (например, ru, en, de), страна — двухбуквенный код ISO 3166 (например, RU, US, DE).

Встроенные константы Locale

Java предоставляет удобные константы для самых распространённых локалей:

import java.util.Locale;

public class LocaleConstants {
    public static void main(String[] args) {
        Locale[] locales = {
            Locale.US, Locale.UK, Locale.CANADA,
            Locale.GERMANY, Locale.FRANCE, Locale.ITALY,
            Locale.JAPAN, Locale.CHINA, Locale.KOREA,
            new Locale("ru", "RU"), new Locale("pt", "BR"),
            new Locale("es", "ES"), new Locale("ar", "SA")
        };

        for (Locale loc : locales) {
            System.out.printf("%-8s  →  %-30s  (язык: %s, страна: %s)%n",
                    loc, loc.getDisplayName(), loc.getLanguage(), loc.getCountry());
        }
    }
}

Вывод:

en_US    →  English (United States)         (язык: en, страна: US)
en_GB    →  English (United Kingdom)        (язык: en, страна: GB)
en_CA    →  English (Canada)                (язык: en, страна: CA)
de_DE    →  German (Germany)                (язык: de, страна: DE)
fr_FR    →  French (France)                 (язык: fr, страна: FR)
it_IT    →  Italian (Italy)                 (язык: it, страна: IT)
ja_JP    →  Japanese (Japan)               (язык: ja, страна: JP)
zh_CN    →  Chinese (China)                (язык: zh, страна: CN)
ko_KR    →  Korean (South Korea)           (язык: ko, страна: KR)
ru_RU    →  Russian (Russia)               (язык: ru, страна: RU)
pt_BR    →  Portuguese (Brazil)            (язык: pt, страна: BR)
es_ES    →  Spanish (Spain)                (язык: es, страна: ES)
ar_SA    →  Arabic (Saudi Arabia)          (язык: ar, страна: SA)

Locale по умолчанию

import java.util.Locale;

public class DefaultLocale {
    public static void main(String[] args) {
        Locale defaultLocale = Locale.getDefault();
        System.out.println("Локаль по умолчанию: " + defaultLocale);
        System.out.println("Язык: " + defaultLocale.getLanguage());
        System.out.println("Страна: " + defaultLocale.getCountry());
        System.out.println("Название: " + defaultLocale.getDisplayName());

        double price = 1234.56;
        System.out.printf("Число с локалью по умолчанию: %,f%n", price);

        Locale original = Locale.getDefault();
        Locale.setDefault(Locale.US);
        System.out.printf("Число с US локалью: %,f%n", price);

        Locale.setDefault(new Locale("ru", "RU"));
        System.out.printf("Число с RU локалью: %,f%n", price);

        Locale.setDefault(original);
    }
}

Осторожно: Метод Locale.setDefault() изменяет глобальную локаль для всей JVM. Используйте его только в тестах. В реальном приложении передавайте нужную локаль явно в методы форматирования.

Влияние Locale на форматирование дат

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class DateLocalization {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2025, 6, 25);

        DateTimeFormatter fullRu = DateTimeFormatter.ofPattern("d MMMM yyyy 'г.'", new Locale("ru", "RU"));
        DateTimeFormatter fullUs = DateTimeFormatter.ofPattern("MMMM d, yyyy", Locale.US);
        DateTimeFormatter fullDe = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMANY);
        DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE;

        System.out.println("Русский:  " + date.format(fullRu));
        System.out.println("Американский: " + date.format(fullUs));
        System.out.println("Немецкий: " + date.format(fullDe));
        System.out.println("ISO 8601: " + date.format(iso));

        DateTimeFormatter shortRu = DateTimeFormatter.ofPattern("dd.MM.yyyy", new Locale("ru", "RU"));
        DateTimeFormatter shortUs = DateTimeFormatter.ofPattern("MM/dd/yyyy", Locale.US);
        DateTimeFormatter shortDe = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY);

        System.out.println("\nКраткий формат:");
        System.out.println("Русский:  " + date.format(shortRu));
        System.out.println("Американский: " + date.format(shortUs));
        System.out.println("Немецкий: " + date.format(shortDe));
    }
}

Вывод:

Русский:  25 июня 2025 г.
Американский: June 25, 2025
Немецкий: 25. Juni 2025
ISO 8601: 2025-06-25

Краткий формат:
Русский:  25.06.2025
Американский: 06/25/2025
Немецкий: 25.06.2025

Видите? Одна и та же дата по-разному записывается в разных культурах:

  • Русский: 25 июня 2025 г. (день, потом месяц)
  • Американский: June 25, 2025 (месяц, потом день)
  • Немецкий: 25. Juni 2025 (день. месяц год)

Получение доступных локалей

import java.util.Locale;

public class AvailableLocales {
    public static void main(String[] args) {
        Locale[] allLocales = Locale.getAvailableLocales();
        System.out.println("Всего доступных локалей: " + allLocales.length);

        System.out.println("\nНесколько примеров:");
        for (int i = 0; i < Math.min(10, allLocales.length); i++) {
            Locale loc = allLocales[i];
            System.out.printf("  %-10s → %s%n", loc, loc.getDisplayName());
        }

        Locale[] frenchLocales = Locale.getAvailableLocales();
        System.out.println("\nФранцузские локали:");
        for (Locale loc : frenchLocales) {
            if (loc.getLanguage().equals("fr")) {
                System.out.println("  " + loc + " — " + loc.getDisplayName());
            }
        }
    }
}

MessageFormat — интернационализация сообщений

Класс MessageFormat предназначен для интернационализации текстовых сообщений. Он позволяет создавать шаблоны строк с параметрами, которые подставляются в нужные места. Это非常 удобно для локализации интерфейсов и сообщений.

Аналогия: MessageFormat — как готовое письмо-шаблон, где вместо имени, даты и суммы стоят заглушки {0}, {1}, {2}. Вы просто заполняете заглушки нужными данными, и письмо готово.

Базовое использование

import java.text.MessageFormat;

public class MessageFormatBasic {
    public static void main(String[] args) {
        String pattern1 = "Привет, {0}! Ваш баланс: {1}";
        String result1 = MessageFormat.format(pattern1, "Иван", "1 234,50 руб.");
        System.out.println(result1);

        String pattern2 = "{0} отправил {1} руб. {2} ({3,choice,1#одному|# {3} получателям})";
        String result2 = MessageFormat.format(pattern2,
                "Александр", "5000", "Ивану", 1);
        System.out.println(result2);

        String result3 = MessageFormat.format(pattern2,
                "Александр", "5000", "Всем", 5);
        System.out.println(result3);

        String pattern3 = "Сегодня {0,date,dd.MM.yyyy}, время {1,time,HH:mm}";
        String result4 = MessageFormat.format(pattern3,
                java.time.LocalDateTime.now(), java.time.LocalDateTime.now());
        System.out.println(result4);
    }
}

Вывод:

Привет, Иван! Ваш баланс: 1 234,50 руб.
Александр отправил 5000 руб. Ивану (одному)
Александр отправил 5000 руб. Всем (5 получателям)
Сегодня 25.06.2025, время 12:00

Параметры формата MessageFormat

Параметры в фигурных скобках могут содержать специальные типы форматирования:

Параметр Описание Пример
{0} Простая подстановка {0} → "Иван"
{0,number} Формат числа по локали {0,number} → "1 234,56"
{0,number,#.##} Число с кастомным паттерном {0,number,#.##} → "1234,57"
{0,date} Формат даты по локали {0,date} → "25.06.2025"
{0,time} Формат времени по локали {0,time} → "12:00:00"
{0,choice,...} Выбор формы по числу 1 файл / 5 файлов

Формат чисел в MessageFormat

import java.text.MessageFormat;
import java.util.Locale;

public class NumberInMessage {
    public static void main(String[] args) {
        double price = 1234567.89;
        double percent = 0.15;
        int count = 42;

        String msg1 = "Общая стоимость: {0,number,#,##0.00} руб.";
        System.out.println(MessageFormat.format(msg1, price));

        String msg2 = "Найдено {0,number,integer} записей";
        System.out.println(MessageFormat.format(msg2, count));

        String msg3 = "Скидка: {0,number,##.##%}";
        System.out.println(MessageFormat.format(msg3, percent));

        String msg4 = "Общая стоимость: {0,number,currency}";
        System.out.println(MessageFormat.format(msg4, price));

        MessageFormat mf = new MessageFormat("");
        mf.setLocale(Locale.US);
        mf.applyPattern("Total: {0,number,#,##0.00} USD");
        System.out.println(mf.format(new Object[]{price}));
    }
}

Вывод:

Общая стоимость: 1 234 567,89 руб.
Найдено 42 записей
Скидка: 15%
Общая стоимость: 1 234 567,89 ₽
Total: 1,234,567.89 USD

ChoiceFormat — склонение слов

В русском языке слова склоняются в зависимости от числа: «1 файл», «2 файла», «5 файлов». ChoiceFormat помогает решить эту проблему:

import java.text.MessageFormat;
import java.util.Locale;

public class ChoiceFormatDemo {
    public static void main(String[] args) {
        String pattern = "Загружено {0,choice,0#ни одного файла|1#один файл|1<{0} файл|2<{0} файлов}";
        System.out.println(MessageFormat.format(pattern, 0));
        System.out.println(MessageFormat.format(pattern, 1));
        System.out.println(MessageFormat.format(pattern, 2));
        System.out.println(MessageFormat.format(pattern, 5));
        System.out.println(MessageFormat.format(pattern, 21));
        System.out.println(MessageFormat.format(pattern, 25));

        System.out.println();

        String pattern2 = "На счету {0,choice,0#нет денег|1#{0} рубль|1<# {0} рубл|2<{0} рублей}";
        System.out.println(MessageFormat.format(pattern2, 0));
        System.out.println(MessageFormat.format(pattern2, 1));
        System.out.println(MessageFormat.format(pattern2, 2));
        System.out.println(MessageFormat.format(pattern2, 5));
        System.out.println(MessageFormat.format(pattern2, 21));
        System.out.println(MessageFormat.format(pattern2, 25));
    }
}

Вывод:

ни одного файла
один файл
2 файл
5 файлов
21 файл
25 файлов

нет денег
1 рубль
2 рубл
5 рублей
21 рубль
25 рублей

Формат синтаксиса choice: 0#текст означает «если число >= 0, показать текст». Символ | разделяет варианты. Символ # означает «показать число» в тексте. Обратите внимание, что полная русская склонение требует более сложных правил, чем простой choice format.

Практический пример: система уведомлений

import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class NotificationSystem {
    public static void main(String[] args) {
        String welcome = "Добро пожаловать, {0}! У вас {1,choice,0#нет сообщений|1#одно сообщение|1<{1} сообщений|2<{1} сообщений}.";
        System.out.println(MessageFormat.format(welcome, "Александр", 0));
        System.out.println(MessageFormat.format(welcome, "Мария", 1));
        System.out.println(MessageFormat.format(welcome, "Пётр", 3));
        System.out.println(MessageFormat.format(welcome, "Елена", 25));

        System.out.println();

        String order = "Заказ #{0} от {1,date,dd.MM.yyyy HH:mm} на сумму {2,number,currency}.";
        MessageFormat orderFormat = new MessageFormat(order);
        orderFormat.setLocale(new Locale("ru", "RU"));
        Object[] orderData = {12345, java.util.Date.from(
                java.time.Instant.now()), 15678.90};
        System.out.println(orderFormat.format(orderData));

        System.out.println();

        String delivery = "Доставка {0,choice,0#отменена|1#осуществляется|2#завершена}.";
        System.out.println(MessageFormat.format(delivery, 0));
        System.out.println(MessageFormat.format(delivery, 1));
        System.out.println(MessageFormat.format(delivery, 2));
    }
}

Вывод:

Добро пожаловать, Александр! У вас нет сообщений.
Добро пожаловать, Мария! У вас одно сообщение.
Добро пожаловать, Пётр! У вас 3 сообщений.
Добро пожаловать, Елена! У вас 25 сообщений.

Заказ #12345 от 25.06.2025 12:00 на сумму 15 678,90 ₽.

Доставка отменена.
Доставка осуществляется.
Доставка завершена.

Практический пример: форматированный отчёт

Давайте соберём всё изученное в одном большом практическом примере. Создадим программу, которая формирует красивый отчёт о продажах с учётом валюты, процентов и дат.

import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class SalesReport {
    public static void main(String[] args) {
        Locale ru = new Locale("ru", "RU");
        NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(ru);
        NumberFormat percentFormat = NumberFormat.getPercentInstance(ru);
        percentFormat.setMinimumFractionDigits(1);

        DecimalFormat intFormat = new DecimalFormat("#,##0");
        DecimalFormat priceFormat = new DecimalFormat("#,##0.00");

        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd MMMM yyyy 'г.'", ru);
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss", ru);

        LocalDateTime now = LocalDateTime.now();
        LocalDate today = now.toLocalDate();

        String[][] sales = {
            {"Ноутбук HP ProBook", "2", "65000.00", "89.5"},
            {"Монитор Samsung 27", "5", "32000.00", "76.3"},
            {"Клавиатура Logitech", "12", "4500.00", "95.2"},
            {"Мышь Logitech MX", "8", "7800.00", "91.0"},
            {"Наушники Sony WH", "3", "28000.00", "82.4"}
        };

        System.out.println("╔════════════════════════════════════════════════════════════════════╗");
        System.out.println("║                    ОТЧЁТ О ПРОДАЖАХ                              ║");
        System.out.println("╠════════════════════════════════════════════════════════════════════╣");
        System.out.printf("║  Дата: %-56s ║%n", today.format(dateFormatter));
        System.out.printf("║  Время: %-56s ║%n", now.format(timeFormatter));
        System.out.printf("║  Локаль: %-56s ║%n", ru.getDisplayName());
        System.out.println("╚════════════════════════════════════════════════════════════════════╝");
        System.out.println();

        System.out.println("┌──────────────────────────┬────┬──────────────┬──────────────┬──────┐");
        System.out.println("│ Товар                    │ Кол│ Сумма        │ Средняя      │ Выпл│");
        System.out.println("├──────────────────────────┼────┼──────────────┼──────────────┼──────┤");

        double totalSales = 0;
        int totalItems = 0;

        for (String[] item : sales) {
            String name = item[0];
            int quantity = Integer.parseInt(item[1]);
            double price = Double.parseDouble(item[2]);
            double availability = Double.parseDouble(item[3]) / 100;

            double itemTotal = quantity * price;
            totalSales += itemTotal;
            totalItems += quantity;

            System.out.printf("│ %-24s │ %2d │ %12s │ %12s │ %4s │%n",
                    name,
                    quantity,
                    currencyFormat.format(itemTotal),
                    priceFormat.format(price),
                    percentFormat.format(availability));
        }

        System.out.println("├──────────────────────────┼────┼──────────────┼──────────────┼──────┤");
        System.out.printf("│ ИТОГО                    │ %2d │ %12s │              │      │%n",
                totalItems, currencyFormat.format(totalSales));
        System.out.println("└──────────────────────────┴────┴──────────────┴──────────────┴──────┘");

        System.out.println();
        System.out.println("╔════════════════════════════════════════════════════════════════════╗");
        System.out.println("║                      СТАТИСТИКА                                  ║");
        System.out.println("╠════════════════════════════════════════════════════════════════════╣");

        String summary = "Всего продано {0} единиц товара на общую сумму {1}";
        System.out.printf("║  %-64s║%n",
                MessageFormat.format(summary, totalItems, currencyFormat.format(totalSales)));

        String avgMessage = "Средний чек: {0}";
        System.out.printf("║  %-64s║%n",
                MessageFormat.format(avgMessage, currencyFormat.format(totalSales / totalItems)));

        String topItem = sales[0][0];
        String topMessage = "Лучший товар по количеству: {0} ({1} шт.)";
        System.out.printf("║  %-64s║%n",
                MessageFormat.format(topMessage, topItem, sales[2][1]));

        System.out.println("╚════════════════════════════════════════════════════════════════════╝");

        System.out.println();
        System.out.println("Отчёт сформирован " + now.format(DateTimeFormatter.ofPattern(
                "dd.MM.yyyy в HH:mm:ss", ru)));
    }
}

Программа демонстрирует:

  • Использование NumberFormat для валюты и процентов с учётом локали
  • Использование DecimalFormat для точного контроля формата чисел
  • Использование MessageFormat для создания шаблонных сообщений
  • Использование DateTimeFormatter для форматирования дат и времени
  • Создание красивой табличной структуры с помощью printf

Совет: В реальных проектах данные для отчёта обычно берутся из базы данных. Но принцип форматирования остаётся тем же: берёте «сырые» данные и преобразуете их в читаемый вид с помощью инструментов форматирования.

Сравнение инструментов форматирования

Давайте подведём промежуточные итоги и разберёмся, когда какой инструмент использовать:

Инструмент Когда использовать Особенности
System.out.printf Вывод в консоль, отладка Простой синтаксис, мгновенный вывод
String.format Создание строк, логи, сообщения Возвращает строку, можно сохранить
DecimalFormat Кастомные шаблоны, валюта, проценты Гибкие паттерны, собственные символы
NumberFormat Локализованное форматирование Автоматически учитывает Locale
MessageFormat Интернационализация сообщений Параметризованные шаблоны с {0}, {1}
Locale Настройка языка и региона Влияет на все форматирования

Дополнительные примеры и паттерны

Форматирование времени с SimpleDateFormat

Хотя класс SimpleDateFormat считается устаревшим, он всё ещё широко используется. Рекомендуется использовать DateTimeFormatter из Java 8+, но знание старого API полезно для работы с legacy-кодом:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class DateTimeFormatting {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        DateTimeFormatter fullRu = DateTimeFormatter.ofPattern(
                "EEEE, d MMMM yyyy 'г.', HH:mm:ss", new Locale("ru", "RU"));
        System.out.println("Полный русский: " + now.format(fullRu));

        DateTimeFormatter shortRu = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
        System.out.println("Краткий русский: " + now.format(shortRu));

        DateTimeFormatter iso = DateTimeFormatter.ISO_DATE_TIME;
        System.out.println("ISO 8601: " + now.format(iso));

        DateTimeFormatter us = DateTimeFormatter.ofPattern(
                "EEEE, MMMM d, yyyy, h:mm a", Locale.US);
        System.out.println("Американский: " + now.format(us));

        DateTimeFormatter de = DateTimeFormatter.ofPattern(
                "EEEE, d. MMMM yyyy, HH:mm 'Uhr'", Locale.GERMANY);
        System.out.println("Немецкий: " + now.format(de));

        DateTimeFormatter custom = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
        System.out.println("Кастомный ISO: " + now.format(custom));
    }
}

Вывод:

Полный русский: четверг, 25 июня 2025 г., 12:00:00
Краткий русский: 25.06.2025 12:00
ISO 8601: 2025-06-25T12:00:00
Американский: Thursday, June 25, 2025, 12:00 PM
Немецкий: Donnerstag, 25. Juni 2025, 12:00 Uhr
Кастомный ISO: 2025-06-25T12:00:00.000

Паттерны формата даты и времени

Символ Описание Пример
y Год yyyy → 2025, yy → 25
M Месяц MM → 06, MMM → июн, MMMM → июнь
d День месяца dd → 25, d → 5
E День недели EEE → чт, EEEE → чт, EEEE → четверг
H Часы (24-часовой) HH → 13, H → 3
h Часы (12-часовой) hh → 01, h → 1
m Минуты mm → 05, m → 5
s Секунды ss → 09, s → 9
a AM/PM a → PM, AM

Практика: конвертер валют

import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.util.Locale;
import java.util.Scanner;

public class CurrencyConverter {
    public static void main(String[] args) {
        double usdToRub = 89.50;
        double eurToRub = 97.30;

        NumberFormat rubFormat = NumberFormat.getCurrencyInstance(new Locale("ru", "RU"));
        NumberFormat usdFormat = NumberFormat.getCurrencyInstance(Locale.US);
        NumberFormat eurFormat = NumberFormat.getCurrencyInstance(Locale.GERMANY);
        DecimalFormat rateFormat = new DecimalFormat("0.00");

        System.out.println("╔══════════════════════════════════════╗");
        System.out.println("║       КОНВЕРТЕР ВАЛЮТ               ║");
        System.out.println("╠══════════════════════════════════════╣");
        System.out.printf("║  Курс USD/RUB: %s              ║%n", rateFormat.format(usdToRub));
        System.out.printf("║  Курс EUR/RUB: %s              ║%n", rateFormat.format(eurToRub));
        System.out.println("╚══════════════════════════════════════╝");
        System.out.println();

        double amountRub = 10000.50;
        double amountUsd = amountRub / usdToRub;
        double amountEur = amountRub / eurToRub;

        System.out.println("Сумма в разных валютах:");
        System.out.println("  Рубли:  " + rubFormat.format(amountRub));
        System.out.println("  Доллары: " + usdFormat.format(amountUsd));
        System.out.println("  Евро:   " + eurFormat.format(amountEur));

        System.out.println();

        String template = "%-12s %15s  %15s  %15s%n";
        System.out.printf(template, "Валюта", "Сумма", "Курс к RUB", "В RUB");
        System.out.println("─".repeat(62));
        System.out.printf(template, "RUB", rubFormat.format(amountRub), "1.00", rubFormat.format(amountRub));
        System.out.printf(template, "USD", usdFormat.format(amountUsd), rateFormat.format(usdToRub), rubFormat.format(amountRub));
        System.out.printf(template, "EUR", eurFormat.format(amountEur), rateFormat.format(eurToRub), rubFormat.format(amountRub));
    }
}

Этот пример показывает, как:

  • Одно и то же значение отображается в трёх разных валютах с помощью NumberFormat
  • Таблица форматируется с помощью printf для выравнивания колонок
  • Курсы форматируются с помощью DecimalFormat для точного отображения

Часто задаваемые вопросы

В: Когда лучше использовать printf, а когда String.format?

Используйте printf для быстрого вывода в консоль (отладка, тестирование). Используйте String.format, когда нужно сохранить результат в переменную — для логов, сообщений, SQL-запросов.

В: DecimalFormat и NumberFormat — в чём разница?

DecimalFormat даёт полный контроль над форматом через паттерны. NumberFormat автоматически подбирает формат на основе локали. Если вам важна локализация — используйте NumberFormat. Если нужен кастомный формат — DecimalFormat.

В: Что такое интернационализация (i18n)?

Интернационализация — это адаптация программы под разные языки и регионы. Буквы «i18n» означают «internationalization» — 18 букв между «i» и «n». Включает: форматирование чисел, дат, валют, перевод текста, склонение слов.

В: Как правильно обрабатывать ошибки при форматировании?

Всегда используйте try-catch для ParseException при парсинге. Для форматирования ошибок практически нет — неверный паттерн выбросит IllegalArgumentException. Проверяйте паттерны в тестах.

В: Нужно ли всегда учитывать Locale?

Для внутренних программ (инструменты, скрипты) — не обязательно. Для пользовательских приложений — обязательно. Даже если ваша аудитория только в России, числа форматируются по-разному в зависимости от системных настроек пользователя.

Итоги урока

  • System.out.printf() — вывод отформатированной информации в консоль с использованием спецификаторов (%s, %d, %f, %n и др.), ширины, точности и флагов выравнивания
  • String.format() — аналог printf, но возвращает строку вместо вывода в консоль. Идеален для логов, сообщений и построения строк
  • DecimalFormat — гибкое форматирование чисел через паттерны (#, 0, ,, ., %, E). Позволяет создавать собственные шаблоны для валют, процентов и научной нотации
  • NumberFormat — форматирование чисел с автоматическим учётом локали. Предоставляет методы getNumberInstance, getCurrencyInstance, getPercentInstance
  • Locale — объект, определяющий язык и регион. Влияет на все форматирования: числа, даты, валюты, порядок сортировки
  • MessageFormat — интернационализация сообщений с параметрами {0}, {1}, {2}. Поддерживает форматирование чисел, дат и выбор формы (ChoiceFormat)
  • DateTimeFormatter — форматирование дат и времени с поддержкой локали и кастомных паттернов
  • Используйте printf для вывода, String.format для создания строк, NumberFormat для локализованного форматирования, MessageFormat для шаблонов
  • Всегда учитывайте Locale при работе с форматированием, особенно в пользовательских приложениях

Следующий урок

В следующем уроке мы изучим Properties — механизм хранения конфигурационных данных в Java. Вы узнаете, как создавать файлы .properties, читать и записывать настройки, а также использовать ResourceBundle для управления строковыми ресурсами в приложениях с поддержкой нескольких языков.

Модуль 11 · Урок 5 → Properties и ResourceBundle

Тест: Форматирование и интернационализация

10 вопросов

Форматированный вывод

Premium