W3docs

Java ZonedDateTime

Работа с датой и временем с учётом часового пояса в Java через ZonedDateTime и ZoneId.

ZonedDateTime — это LocalDateTime с прикреплённым ZoneId. Он означает: «эта календарная дата и время в этом месте». Комбинация однозначно определяет единственный момент на глобальной временной шкале — 2025-11-04T14:00 [America/New_York] — это ровно один Instant, отличный от 2025-11-04T14:00 [Europe/Berlin].

Этот класс нужен, когда важно местное время события в конкретном месте. Календари встреч. Запланированные задания, которые должны срабатывать «в 9 утра по часовому поясу пользователя». Всё, что должно корректно пережить переход на летнее время. LocalDateTime не знает часового пояса; Instant хранит время в UTC и не несёт понятной человеку метки зоны. ZonedDateTime — это оба сразу.

ZoneId: каталог часовых поясов

Прежде чем перейти к ZonedDateTime, познакомимся с ZoneId — часовой пояс идентифицируется через ZoneId, который получают вызовом ZoneId.of(...):

ZoneId ny    = ZoneId.of("America/New_York");
ZoneId de    = ZoneId.of("Europe/Berlin");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZoneId utc   = ZoneId.of("UTC");
ZoneId sys   = ZoneId.systemDefault();

Строки — это идентификаторы базы данных часовых поясов IANA (Регион/Город). Полный список возвращает ZoneId.getAvailableZoneIds() — около 600 записей, обновляемых по мере того, как страны меняют свои зоны или правила перехода на летнее время. ZoneId хранит историческую информацию из IANA, поэтому даты в 1985 году используют правила, действовавшие в 1985 году.

Не используйте ZoneOffset (фиксированное смещение ±HH:MM), когда имеете в виду реальный часовой пояс. ZoneOffset.of("-05:00") правилен для Нью-Йорка в ноябре, но неверен в июне; ZoneId.of("America/New_York") корректен круглый год.

Трёхбуквенные названия зон, такие как "EST" и "PST", в основном являются псевдонимами, неоднозначными (это Eastern Standard или Eastern Australia?) и тихо устаревшими. Используйте Регион/Город. Значения "UTC" и "GMT" — исключения и допустимы.

Создание

ZonedDateTime now    = ZonedDateTime.now();                                  // system zone
ZonedDateTime nowNY  = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime made   = ZonedDateTime.of(2025, 11, 4, 14, 0, 0, 0, ZoneId.of("America/New_York"));
ZonedDateTime parsed = ZonedDateTime.parse("2025-11-04T14:00:00-05:00[America/New_York]");

Самый распространённый путь создания — «есть LocalDateTime, есть ZoneId, соединить их»:

LocalDateTime ldt = LocalDateTime.of(2025, 11, 4, 14, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));

atZone(zone) — это единственный вызов, переходящий от показания местных часов к зонированному моменту. Он также обрабатывает два граничных случая, которые вносит переход на летнее время.

Переход на летнее время: когда часы пропускают или повторяют время

Дважды в год в зонах, соблюдающих переход на летнее время, часы перепрыгивают вперёд или назад. Когда часы переводят вперёд — в США 02:00 прыгает к 03:00 в воскресенье в марте — времена между 02:00 и 03:00 не существуют в этот день. Когда часы переводят назад, времена между 01:00 и 02:00 происходят дважды. ZonedDateTime должен что-то делать в обоих случаях, и это задокументировано:

  • Пропущенное время (пробел): atZone возвращает время после перехода. LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York")) становится 03:30-04:00 — JDK сдвинул время вперёд на час, чтобы попасть на допустимое значение.
  • Повторяющееся время (перекрытие): atZone возвращает более ранний из двух допустимых моментов (тот, что до изменения смещения). Используйте withEarlierOffsetAtOverlap() или withLaterOffsetAtOverlap() для явного выбора.
ZonedDateTime ambiguous = LocalDateTime.of(2025, 11, 2, 1, 30)
    .atZone(ZoneId.of("America/New_York"));                  // 01:30 EDT (earlier)
ZonedDateTime explicit = ambiguous.withLaterOffsetAtOverlap();   // 01:30 EST (later)

Два ZonedDateTime имеют одинаковый LocalDateTime, но разные смещения и разные Instant. Это единственное место в java.time, где одно и то же показание местных часов законно соответствует двум моментам — и именно здесь кроется источник ошибок, связанных с летним временем. Будьте внимательны, когда перекрытие имеет значение.

Разбор на части

ZoneId zone = zdt.getZone();
ZoneOffset offset = zdt.getOffset();
LocalDateTime ldt = zdt.toLocalDateTime();
LocalDate date = zdt.toLocalDate();
LocalTime time = zdt.toLocalTime();
Instant inst = zdt.toInstant();
OffsetDateTime odt = zdt.toOffsetDateTime();

Методы доступа делятся на три группы: зонная половина (getZone, getOffset), половина местных часов (toLocalDateTime, toLocalDate, toLocalTime) и половина глобального момента (toInstant). Все три одновременно справедливы для одного ZonedDateTime; вы выбираете нужную проекцию.

OffsetDateTime — связанный тип: LocalDateTime плюс ZoneOffset (без зоны, без перехода на летнее время). Он удобен для сериализации «2025-11-04T14:00-05:00» без привязки к именованной зоне (часто это то, что нужно временным меткам JSON); для кода, требующего арифметики с учётом летнего времени, сохраняйте ZonedDateTime.

Два смысла понятия «следующий день»

У ZonedDateTime есть два похожих, но разных метода:

zdt.plusDays(1);                                              // add 1 day to the local clock reading
zdt.plus(Duration.ofHours(24));                                // add exactly 24 hours

В день перехода на летнее время они дают разный результат. В день, когда часы переводят вперёд, plusDays(1) приводит к тому же местному времени завтра (до которого всего 23 часа реального времени). plus(Duration.ofHours(24)) приводит к показанию часов на час позже вчерашнего.

ЦельМетод
«То же время завтра» (календарь)plusDays(1)
«Ровно через 24 часа» (продолжительность)plus(Duration.ofHours(24))

Оба корректны; они отвечают на разные вопросы. Выбирайте осознанно.

Сравнение и равенство

zdt1.isBefore(zdt2);                                          // compares Instants
zdt1.isAfter(zdt2);
zdt1.isEqual(zdt2);                                           // compares Instants
zdt1.equals(zdt2);                                            // compares LocalDateTime + Zone + Offset

Различие принципиальное:

  • isBefore/isAfter/isEqual сравнивают лежащие в основе моменты (Instant).
  • equals сравнивает полную структуру — два ZonedDateTime, представляющих один и тот же момент, но в разных зонах, не являются equal.

Чтобы проверить «одинаковый ли момент вне зависимости от зоны», используйте isEqual или преобразуйте оба в Instant и сравните.

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

Программа ниже планирует встречу на 14:00 по берлинскому времени и вычисляет, которое время это будет в офисах Нью-Йорка и Токио. Затем она планирует еженедельную встречу, которая корректно проходит через переход на летнее время, демонстрируя разницу между plusDays(7) и plus(Duration.ofDays(7)) на переходной неделе.

java— editable, runs on the server

Что следует вынести из запуска:

  • withZoneSameInstant(otherZone) — это операция «который час в их офисе?»: она фиксирует момент и отображает показание часов в новой зоне. Её аналог withZoneSameLocal(otherZone) фиксирует показание часов и изменяет момент (встреча перемещается). Названия легко перепутать; разница в том, что остаётся неизменным. Внимательно читайте их, когда пишете.
  • berlin.equals(ny) дал false, хотя оба представляли один момент. equals сравнивает полную структуру (локальные дата-время + зона). Для проверки «одинаковый ли момент вне зависимости от метки» используйте isEqual или сравнивайте Instant. Это то же самое различие, что LocalDate.equals и isEqual: equals — для «одинаковых объектов-значений», isEqual — для «одинаковых точек во времени».
  • Пробел перехода на летнее время (2025-03-09 02:30 в NY) был разрешён перемещением к 03:30-04:00. JDK не выбросил исключение; он выбрал момент после перехода. Если вам принципиально важно обнаружить, что вы передали невозможное время, используйте ZoneRules.getTransition(localDateTime) и проверьте, является ли возвращённый объект пробелом.
  • Перекрытие перехода на летнее время (2025-11-02 01:30 в NY) дало два разных ZonedDateTime с одинаковыми локальными полями и разными смещениями — EDT и EST, с разницей в один час. withLaterOffsetAtOverlap() и withEarlierOffsetAtOverlap() позволяют выбрать нужный. Если вы храните запланированные события, заранее определите, какой из двух имеет в виду пользователь, и применяйте правильный метод при разборе.
  • plusDays(1) и plus(Duration.ofHours(24)) дали разный результат в день перевода часов вперёд — 23 часа реального времени против 24, приведя к разным показаниям местных часов. Используйте plusDays/plusWeeks для планирования по календарю («то же время завтра»), и plus(Duration) для арифметики прошедшего времени («будильник через 24 часа»). Выбор почти всегда соответствует намерению, понятному пользователю.

Что дальше

ZonedDateTime — это «человеческая» сторона понятия «момент с меткой». Следующая глава, Java Instant, — это «машинная» сторона: момент как количество наносекунд с начала эпохи, без зоны, без календаря — тип, который используют все распределённые системы при передаче данных.

Практика

Практика
Вы планируете еженедельную встречу в 09:00 America/New_York и сохраняете следующее вхождение с помощью `nextMeeting.plusDays(7)`. На неделе, которая пересекает переход на летнее время (перевод часов вперёд), что справедливо в отношении результата?
Вы планируете еженедельную встречу в 09:00 America/New_York и сохраняете следующее вхождение с помощью `nextMeeting.plusDays(7)`. На неделе, которая пересекает переход на летнее время (перевод часов вперёд), что справедливо в отношении результата?
Was this page helpful?