Введение в 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 покажется вам знакомым.
Две важные вещи в переработке:
- Каждый тип неизменяем. Однажды созданный
LocalDateникогда не меняется. Методы вродеplusDays(7)возвращают новыйLocalDate. Это делает API потокобезопасным по построению и убирает целую категорию ошибок. - Каждый тип означает одно.
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, — и показывает, как они преобразуются друг в друга. Это версия «экскурсия»; каждый отдельный тип отныне получает собственную главу.
Что вынести из запуска:
- Первый блок построил семейство через композицию:
LocalDate+LocalTime=LocalDateTime;LocalDateTime+ZoneId=ZonedDateTime;ZonedDateTime→Instant. Это решётка преобразований, и вы будете выполнять их каждый раз, пересекая границу 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?