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