Введение в 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 покажется вам знакомым.
Два ключевых изменения в дизайне:
- Каждый тип неизменяем. Созданный однажды
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 — наиболее важное. Локальные типы не содержат информации о часовом поясе. 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 — и показывает, как они преобразуются друг в друга. Это «обзорная» версия; каждому отдельному типу посвящена своя глава.
Что стоит вынести из этого запуска:
- Первый блок построил семейство путём композиции:
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 — самый простой из пяти типов «момента во времени» и лучшее место для изучения fluent-стиля, который разделяют все остальные.