Форматирование дат в Java
Форматируйте даты и время в строки с помощью DateTimeFormatter и стандартных или пользовательских шаблонов.
Форматирование дат — это преобразование значения даты/времени в строку, понятную человеку. В этой главе рассматривается DateTimeFormatter — как его создать (встроенный, локализованный или на основе шаблона), полный алфавит шаблонов, работа с локалями и часовыми поясами, а также типичные ошибки, приводящие к некорректному выводу. Форматировщик работает с каждым типом из java.time: LocalDate, LocalTime, LocalDateTime, ZonedDateTime и Instant.
У каждого типа java.time есть метод toString(), который возвращает представление в формате ISO-8601: 2025-11-04, 14:30:00, 2025-11-04T14:30:00Z. Это удобно для логов и передачи данных между машинами. Для отображения, понятного пользователю ("November 4, 2025" или "4 Nov, 14:30"), нужен форматировщик.
Класс — java.time.format.DateTimeFormatter. Это современная, потокобезопасная, неизменяемая замена устаревшему java.text.SimpleDateFormat (который не был потокобезопасным и порождал трудноуловимые баги в продакшене при совместном использовании). Храните экземпляр как static final и используйте его из нескольких потоков без какой-либо синхронизации или защитного копирования.
Три способа создать форматировщик
// 1. Built-in ISO formatters
DateTimeFormatter.ISO_LOCAL_DATE; // 2025-11-04
DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 2025-11-04T14:30:00
DateTimeFormatter.ISO_OFFSET_DATE_TIME; // 2025-11-04T14:30:00-05:00
DateTimeFormatter.ISO_ZONED_DATE_TIME; // 2025-11-04T14:30:00-05:00[America/New_York]
DateTimeFormatter.ISO_INSTANT; // 2025-11-04T19:30:00Z
DateTimeFormatter.BASIC_ISO_DATE; // 20251104
// 2. Localised formatters
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); // November 4, 2025 (en-US)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); // Nov 4, 2025, 2:30:00 PM
// 3. Pattern-based formatters
DateTimeFormatter.ofPattern("dd MMM yyyy"); // 04 Nov 2025
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzz"); // 2025-11-04 14:30 ESTAPI на основе шаблонов используется чаще всего. Локализованный вариант подходит, когда нужен формат, соответствующий культуре пользователя, и вы хотите, чтобы JDK сам выбрал структуру.
Форматирование
Форма вызова симметрична с обеих сторон:
String s = formatter.format(temporal);
String s2 = temporal.format(formatter); // same thing, fluent styleОба варианта работают. Большинство кода использует fluent-форму.
LocalDate today = LocalDate.now();
String us = today.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); // 11/04/2025
String iso = today.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2025-11-04
String eu = today.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")); // 04.11.2025Алфавит шаблонов
Большая таблица, к которой вы будете возвращаться. Буквы чувствительны к регистру, и количество повторений имеет значение.
| Буква | Значение | Пример |
|---|---|---|
y | год | y → 2025, yy → 25, yyyy → 2025 |
M | месяц | M → 11, MM → 11, MMM → Nov, MMMM → November |
d | день месяца | d → 4, dd → 04 |
E | день недели | E → Tue, EEEE → Tuesday |
H | час 0-23 | H → 14, HH → 14 |
h | час 1-12 | h → 2, hh → 02 (используется с a) |
a | AM/PM | a → PM |
m | минуты | m → 5, mm → 05 |
s | секунды | s → 9, ss → 09 |
S | доли секунды | SSS → 123 (миллисекунды) |
n | наносекунды | nnnnnnnnn → 123456789 |
z | название зоны | z → EST, zzzz → Eastern Standard Time |
Z | смещение зоны | Z → -0500, ZZ → -0500, ZZZZ → GMT-05:00 |
X | ISO-смещение | X → -05, XX → -0500, XXX → -05:00 |
V | идентификатор зоны | VV → America/New_York |
Литеральный текст заключается в одинарные кавычки:
DateTimeFormatter.ofPattern("EEEE, MMMM d 'at' h:mm a"); // Tuesday, November 4 at 2:30 PMДля вывода самой одинарной кавычки используйте две подряд: ''.
Самая распространённая путаница — m против M (строчная = минуты, прописная = месяц) и H против h (прописная = 0–23, строчная = 1–12). Большинство ошибок вида «время отображается неправильно» вызваны именно этими опечатками.
Локализация: Locale и withLocale
Форматировщик использует локаль JVM по умолчанию, если не указана другая. Чтобы вывод был «всегда на английском» или «всегда на немецком», явно задайте локаль:
DateTimeFormatter english = DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.US);
DateTimeFormatter german = DateTimeFormatter.ofPattern("EEEE, d. MMMM", Locale.GERMAN);
DateTimeFormatter french = DateTimeFormatter.ofPattern("EEEE d MMMM", Locale.FRENCH);
today.format(english); // Tuesday, November 4
today.format(german); // Dienstag, 4. November
today.format(french); // mardi 4 novembreДля серверного рендеринга всегда передавайте локаль. «Локаль JVM по умолчанию» непредсказуема на продакшен-серверах и является источником ошибок вида «на моём ноутбуке всё правильно, на сервере — нет».
Отображение часового пояса
ZonedDateTime и Instant — единственные типы, содержащие информацию о часовом поясе. Форматирование LocalDateTime с шаблоном, включающим z или Z, выбросит исключение — поясу нечего печатать. Сначала выполните преобразование:
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));
zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z")); // 2025-11-04 14:30 ESTДля Instant форматировщику тоже нужна зона — у Instant её нет, поэтому форматировщики для отображения, включающие зависящие от зоны поля, требуют withZone:
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
.withZone(ZoneId.of("America/New_York"));
f.format(Instant.now()); // formatter supplies the zone for displayБез withZone форматирование Instant с календарным шаблоном выбросит исключение.
Стилизованные форматировщики с FormatStyle
Локализованные фабрики предоставляют четыре стандартных размера:
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); // 11/4/25 (en-US), 04.11.25 (de-DE)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); // Nov 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); // November 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL); // Tuesday, November 4, 2025Те же четыре размера есть у ofLocalizedTime и ofLocalizedDateTime. Используйте их, когда хотите, чтобы структура отображения следовала локали пользователя, а не навязывала единственный формат. Сочетайте с .withLocale(...) для явного указания локали.
Практический пример: одна дата, шесть вариантов отображения
Программа ниже форматирует один ZonedDateTime шестью распространёнными способами: ISO для машинных логов, 12-часовой формат для американских пользователей, 24-часовой европейский для немецких, длинная локализованная форма, пользовательский шаблон со встроенным литеральным текстом и форматировщик Instant через withZone для сырых временны́х меток.
Что следует вынести из запуска:
- Кешированные поля
static final DateTimeFormatter— это правильный подход.DateTimeFormatterнеизменяем и потокобезопасен; его создание недорого, но не бесплатно, поэтому повторное использование одного экземпляра — рекомендованный JDK-паттерн. Не создавайте новый экземпляр внутри горячего цикла. - Один и тот же
ZonedDateTimeдал шесть разных строк в зависимости от форматировщика. Объект-значение не изменился; только форматировщик управляет представлением. В этом и состоит разделение, ради которого существуетDateTimeFormatter— держите тип значения чистым, а представление передавайте форматировщику. - Блок «common typo» напечатал
14:11дляHH:MM, потому чтоM— это месяц, а не минуты. Это самая распространённая путаница в алфавите шаблонов. Если отображаемое время выглядит подозрительно похожим на компонент даты — проверьте регистр букв в шаблоне. - Лестница
FormatStyleвыдала четыре строки прогрессирующей длины. ИспользуйтеFormatStyle.MEDIUMкак разумный вариант по умолчанию для «показать дату пользователю, не задумываясь»;LONGиFULL— там, где год и день недели должны быть однозначными;SHORT— в узких элементах интерфейса. LocalDateTimeс шаблоном, содержащим зону, выбросил исключение — форматировщику нужны данные о зоне, а вLocalDateTimeих нет. Решение: преобразовать (ldt.atZone(zone)) или убрать зависящее от зоны поле из шаблона. В любом случае сбой обнаруживается сразу в рантайме.
Что дальше
Форматирование — это направление значение → строка. Следующая глава, Java Date Parsing, — обратная операция: строка → значение — с теми же шаблонами DateTimeFormatter и теми же оговорками. Вместе они образуют I/O-границу для любого кода, обменивающегося датами с пользователями, конфигами, логами или удалёнными API.