W3docs

Введение в Java API даты и времени

Введение в современный Java API даты и времени в java.time, заменяющий устаревшие классы Date и Calendar.

Введение в Java API даты и времени

Java 8 добавила java.time — новый пакет для представления дат, времён, длительностей, часовых поясов и арифметики между ними. Он заменил два прежних API — java.util.Date и java.util.Calendar, — которые заслуженно слыли наихудше спроектированным уголком JDK. Новый API был вдохновлён более ранней библиотекой Joda-Time Стивена Колборна; если вы пользовались Joda, java.time покажется вам знакомым.

Две важные вещи в переработке:

  1. Каждый тип неизменяем. Однажды созданный LocalDate никогда не меняется. Методы вроде plusDays(7) возвращают новый LocalDate. Это делает API потокобезопасным по построению и убирает целую категорию ошибок.
  2. Каждый тип означает одно. LocalDate — это дата без времени. Instant — это момент на временной шкале. Duration — это промежуток времени. Устаревший Date был всем этим сразу, в зависимости от того, какой конструктор вы использовали; новый API разделяет их, так что тип сам сообщает вам, какого рода значение у вас есть.

Эта глава — карта. Следующие десять глав углубляются в каждый класс.

Основные типы

"A date"           LocalDate            2025-11-04
"A time of day"    LocalTime            14:30:00
"Both, no zone"    LocalDateTime        2025-11-04T14:30:00
"Both, with zone"  ZonedDateTime        2025-11-04T14:30:00-05:00 [America/New_York]
"A moment"         Instant              2025-11-04T19:30:00Z      (UTC, seconds-since-epoch)
"A length of time" Duration             PT1H30M                   (1 hour 30 minutes)
"A length of date" Period               P1Y2M3D                   (1 year 2 months 3 days)

Горизонтальное разделение — Local* против Zoned/Instant — самое важное. Local-типы не несут часового пояса. LocalDate, равный 2025-11-04, — это «четвёртое ноября»; он не говорит, четвёртое ли это в Токио или в Гонолулу. Это правильный тип для дня рождения, даты договора или выбора даты в интерфейсе.

Zoned-типы несут свой пояс. ZonedDateTime — это «этот календарный момент в этом месте», что вам нужно для «встреча назначена на 9 утра в Нью-Йорке». Instant — это момент на глобальной временной шкале — секунды UTC от эпохи, — что вам нужно для журналирования, временных меток сообщений, всего, что должно быть глобально упорядочено без необходимости локальных меток.

Горизонтальное разделение между Duration и Period тоже важно. Duration — это промежуток времени, который можно сравнивать в секундах: PT24H — это ровно 24 × 3600 секунд. Period — это промежуток, выраженный в календарных терминах: P1M (один месяц) — это 30 дней в одних месяцах и 31 в других. Для измерений времени вам нужен Duration. Для «добавить один месяц к дате выставления счёта» вам нужен Period.

Текучая форма

Каждый тип создаётся и изменяется через единообразный текучий (fluent) API:

LocalDate today    = LocalDate.now();
LocalDate stardate = LocalDate.of(2025, 11, 4);
LocalDate parsed   = LocalDate.parse("2025-11-04");

LocalDate nextWeek = today.plusDays(7);                     // immutable: returns a NEW LocalDate
LocalDate lastYear = today.minusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1);            // with* returns a copy with one field changed

boolean before = today.isBefore(stardate);
int year = today.getYear();

Три формы, которые вы будете встречать повсюду:

  • now() — текущее значение из системных часов.
  • of(...) — явные компоненты.
  • parse(...) — из строки (по умолчанию ISO-8601).

И для преобразований:

  • plusX(n) / minusX(n) — арифметика.
  • withX(value) — заменить одно поле.
  • isBefore(other) / isAfter(other) — сравнение.

Эта форма повторяется в LocalDate, LocalTime, LocalDateTime, ZonedDateTime и Instant. Как только вы знаете шаблон, каждый класс читает вам тот же диалект со слегка иным словарём.

Часовые пояса сложны, и API это признаёт

Главная причина, по которой java.util.Date был мучителен, — то, что он пытался сделать часовые пояса невидимыми. Результатом стал знаменитый класс ошибок «сохрани Date, извлеки его на сервере в другом часовом поясе, получи неправильную календарную дату». java.time решает это, делая пояс явным в типе.

Если вы принимаете дату от пользователя и не знаете, в каком он поясе, сохраняйте её как LocalDate. Если он говорит, что это «9 утра по его времени», и вы знаете его пояс, сохраняйте её как ZonedDateTime с поясом. Если вы журналируете серверное событие, сохраняйте его как Instant. Не сохраняйте LocalDateTime в надежде, что часовой пояс «дойдёт»; отсутствующий пояс — это и есть вся ошибка.

Instant now = Instant.now();                                 // unambiguous: a moment in UTC
ZonedDateTime localized = now.atZone(ZoneId.of("Europe/Berlin"));  // a label for that moment in Berlin

Иерархия поясов:

  • ZoneOffset — это фиксированное смещение ±HH:MM от UTC: +05:30, -08:00. Без обработки летнего времени.
  • ZoneId — это именованный пояс: Europe/Berlin, America/New_York. Несёт запись базы данных IANA о том, какое смещение у этого пояса в любой заданный день, включая переходы на летнее время и исторические изменения.

Всегда предпочитайте ZoneId вместо ZoneOffset, когда есть выбор. «America/New_York» корректен через переходы на летнее время; «−05:00» корректен только вне летнего времени.

Устаревшие типы никуда не делись

java.util.Date, java.util.Calendar и java.text.SimpleDateFormat всё ещё существуют. Новый код не должен их использовать — но множество старого кода использует, и вам понадобится взаимодействие. Методы преобразования прямые:

// java.util.Date <-> java.time.Instant
Instant inst = legacyDate.toInstant();
Date    back = Date.from(inst);

// java.util.Calendar -> java.time.ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(
    cal.toInstant(), cal.getTimeZone().toZoneId());

Шаблон односторонний: устаревший → java.time прямолинеен; для всего нового оставайтесь в java.time и преобразуйте только на границе API, где живёт старый код. Главы Устаревший Date и Calendar в конце этой части подробно охватывают этот мост.

Разобранный пример: семейство типов в одной программе

Программа ниже использует каждый тип, представленный картой выше — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period, — и показывает, как они преобразуются друг в друга. Это версия «экскурсия»; каждый отдельный тип отныне получает собственную главу.

java— editable, runs on the server

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

  • Первый блок построил семейство через композицию: LocalDate + LocalTime = LocalDateTime; LocalDateTime + ZoneId = ZonedDateTime; ZonedDateTimeInstant. Это решётка преобразований, и вы будете выполнять их каждый раз, пересекая границу API. Для большинства пар стрелки идут в обе стороны — Instant.atZone(zone) и ZonedDateTime.toLocalDateTime() замыкают петли.
  • Один Instant напечатал три по-разному «выглядящих» времени, если смотреть из Нью-Йорка, Берлина и Токио. В этом и смысл Instant: это момент, не зависящий от того, где вы стоите. ZonedDateTime добавляет метку «где я стою». Путать эти два — ошибка устаревшего Date.
  • Duration напечаталась как PT1H30M, а Period — как P3M. Формат длительности ISO-8601 — PnYnMnDTnHnMnS: всё до T — это календарные единицы (Period), всё после — единицы времени (Duration). Строка дословно совпадает с тем, что возвращает toString(), и дословно с тем, что принимает parse(...).
  • today.plusDays(7) создал другой LocalDate. Печать today сразу после показала, что оригинал не изменился — это и есть гарантия неизменяемости. Каждый plus/minus/with возвращает новый объект; получатель никогда не изменяется. Никакого защитного копирования, никаких забот о потокобезопасности, никогда.
  • ChronoUnit.DAYS.between(today, launch) была операцией «расстояние». Она возвращает long, а не Period, потому что ответ в днях не имеет календарной неоднозначности (в отличие от месяцев, длина которых разнится). Каждая глава в этой части где-нибудь использует ChronoUnit — это каталог единиц времени, о которых говорит API.

Что дальше

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

Practice

Практика

You need to store the moment a server received an HTTP request, so that the log can be sorted globally across servers in different time zones. Which `java.time` type fits?