Урок 9 из 11 базовый около 30 мин

java.time: даты, периоды и форматирование

LocalDate, LocalDateTime и ZonedDateTime, разница между Period и Duration, безопасный разбор и вывод дат по образцу.

Почему не Date

Классы java.util.Date и Calendar изменяемы, считают месяцы с нуля и не различают «дату без времени» и «момент в часовом поясе». Пакет java.time, появившийся в Java 8, устроен иначе: типы неизменяемы, каждый отвечает за одно понятие, а арифметика читается как обычное предложение. В новых программах старые классы использовать не нужно.

Пять типов, которых хватает почти всегда

ТипЧто хранитПример
LocalDateДата без времени и поясадень рождения, дата документа
LocalTimeВремя без датыначало занятия в 19:00
LocalDateTimeДата и время без поясарасписание в пределах одного города
ZonedDateTimeДата, время и часовой поясвстреча участников из разных регионов
InstantМомент на шкале UTCотметки времени в логах и базах
import java.time.*;

var registration = LocalDate.of(2026, 8, 17);
var lessonStart = LocalTime.of(19, 0);
var lesson = LocalDateTime.of(registration, lessonStart);
var moscow = lesson.atZone(ZoneId.of("Europe/Moscow"));
var instant = moscow.toInstant();

System.out.println(registration);   // 2026-08-17
System.out.println(moscow);         // 2026-08-17T19:00+03:00[Europe/Moscow]
System.out.println(instant);        // 2026-08-17T16:00:00Z

Все методы вида plusDays, withMonth, atZone возвращают новый объект. Исходное значение не меняется, поэтому переменную нужно переприсвоить: date = date.plusDays(1).

Period и Duration

Разница между двумя датами измеряется в днях, месяцах и годах — это Period. Разница между двумя моментами измеряется в секундах и наносекундах — это Duration. Их путают чаще всего.

var birthday = LocalDate.of(2008, 3, 12);
var today = LocalDate.of(2026, 9, 5);

var age = Period.between(birthday, today);
System.out.println(age.getYears() + " лет, " + age.getMonths() + " мес.");   // 18 лет, 5 мес.

var start = LocalDateTime.of(2026, 9, 5, 19, 0);
var end = LocalDateTime.of(2026, 9, 5, 20, 35);
var length = Duration.between(start, end);
System.out.println(length.toMinutes());   // 95

long daysLeft = java.time.temporal.ChronoUnit.DAYS.between(today, LocalDate.of(2026, 12, 31));   // 117

Для «сколько дней между датами» удобнее ChronoUnit.DAYS.between: Period раскладывает разницу на годы, месяцы и дни, и поле getDays() покажет только остаток.

Форматирование и разбор

Метод toString у всех типов даёт формат ISO 8601 — он однозначен и подходит для хранения и передачи. Для людей нужен DateTimeFormatter:

import java.time.format.DateTimeFormatter;
import java.util.Locale;

var russian = DateTimeFormatter.ofPattern("d MMMM yyyy 'г.'", Locale.forLanguageTag("ru"));
System.out.println(registration.format(russian));   // 17 августа 2026 г.

var compact = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
System.out.println(lesson.format(compact));         // 17.08.2026 19:00

var parsed = LocalDate.parse("05.09.2026", DateTimeFormatter.ofPattern("dd.MM.yyyy"));
System.out.println(parsed.getDayOfWeek());          // SATURDAY

Буквы образца чувствительны к регистру: MM — месяц, mm — минуты, HH — часы от 0 до 23, hh — от 1 до 12. Ошибка в одной букве не вызывает исключение, а тихо подставляет не то поле, поэтому результат форматирования стоит проверить глазами хотя бы один раз.

Разбор строки неверного формата бросает DateTimeParseException. Обрабатывайте его там, где данные приходят от пользователя, и не пропускайте молча.

Часовые пояса и переход на летнее время

Прибавление часа к ZonedDateTime учитывает правила пояса, а прибавление к LocalDateTime — нет. Для России переходов сейчас нет, но для международных встреч разница реальна:

var berlin = ZonedDateTime.of(2026, 3, 29, 1, 30, 0, 0, ZoneId.of("Europe/Berlin"));
System.out.println(berlin.plusHours(1));   // 2026-03-29T03:30+02:00[Europe/Berlin] — час 02:xx в эту ночь пропущен

Правило для прикладных программ: хранить и передавать моменты как Instant или в UTC, а в пояс пользователя переводить только при показе. Идентификаторы поясов берите из региона, например Europe/Moscow, а не из смещения +03:00: смещение может измениться, а регион — нет.

Полезные мелочи

  • LocalDate.now() берёт системные часы; в тестах передавайте Clock, чтобы дата была предсказуемой: LocalDate.now(clock).
  • date.with(TemporalAdjusters.lastDayOfMonth()) — последний день месяца, next(DayOfWeek.MONDAY) — ближайший понедельник.
  • YearMonth и Year — для отчётов по месяцам и годам без фиктивного дня.
  • date.isLeapYear(), date.lengthOfMonth() — календарные вопросы без ручных расчётов.

Частая ошибка

Хранить дату в строке и сравнивать строки. «05.09.2026» и «17.08.2026» при строковом сравнении расположатся неправильно. Разбирайте строку в LocalDate сразу на входе и сравнивайте методами isBefore, isAfter, compareTo.

Попробуйте сами

  1. Напишите метод, который по дате рождения возвращает полный возраст в годах на сегодняшний день, и проверьте его на дате рождения, которая ещё не наступила в этом году.
  2. Составьте расписание из шести занятий каждый вторник и четверг начиная с 8 сентября 2026 года. Подсказка: TemporalAdjusters.next и цикл.
  3. Отформатируйте текущий момент для трёх поясов: Europe/Moscow, Asia/Vladivostok и America/New_York, используя один и тот же Instant.