W3docs

Введение в Java Date and Time API

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

В 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 — наиболее важное. Локальные типы не содержат информации о часовом поясе. LocalDate со значением 2025-11-04 означает «четвёртое ноября»; он не говорит, четвёртое ли это в Токио или на Гавайях. Это правильный тип для дня рождения, даты контракта или поля выбора даты в UI.

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

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

Fluent-стиль API

Каждый тип создаётся и изменяется через единообразный fluent-интерфейс:

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());

Принцип однонаправленный: legacy → java.time — прямолинейно; для нового кода оставайтесь в java.time и выполняйте преобразование только на границе API, где живёт старый код. Главы Legacy 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 — самый простой из пяти типов «момента во времени» и лучшее место для изучения fluent-стиля, который разделяют все остальные.

Практика

Практика
Вам нужно сохранить момент получения HTTP-запроса сервером, чтобы лог можно было сортировать глобально по серверам в разных часовых поясах. Какой тип `java.time` подходит?
Вам нужно сохранить момент получения HTTP-запроса сервером, чтобы лог можно было сортировать глобально по серверам в разных часовых поясах. Какой тип `java.time` подходит?
Was this page helpful?