Зачем java.time
Старые java.util.Date и Calendar — изменяемые и не потокобезопасные, с ловушками: у Date почти весь API устарел, у Calendar месяцы нумеруются с нуля (январь = 0), а SimpleDateFormat нельзя шарить между потоками. Это десятилетиями было источником багов.
java.time (JSR-310, Java 8) построен иначе: все типы неизменяемы и потокобезопасны, модель явная (отдельно момент, дата, время, зона), а API текучий и предсказуемый. Это стандарт де-факто; старый код переводят на него.
Машинное и человеческое время
Instant — момент на глобальной шкале (секунды/наносекунды от эпохи 1970-01-01T00:00:00Z, всегда UTC). Это «машинное» время: удобно хранить, сравнивать и логировать.
LocalDate (только дата), LocalTime (только время) и LocalDateTime (дата+время) — «человеческое» время БЕЗ зоны. LocalDateTime «2026-01-01 09:00» не задаёт конкретный момент, пока не привязан к зоне: в Москве и в Нью-Йорке это разные моменты.
Instant now = Instant.now(); // момент в UTC
LocalDate today = LocalDate.now(); // дата без времени и зоны
LocalDateTime dt = LocalDateTime.of(2026, 1, 1, 9, 0); // без зоны — ещё не момент
Часовые пояса: ZoneId и ZonedDateTime
ZonedDateTime = LocalDateTime + ZoneId (например, Europe/Moscow): это уже конкретный момент с правилами перехода на летнее время. OffsetDateTime проще — дата-время с фиксированным смещением от UTC (+03:00), без правил DST.
Практическое правило: храните и передавайте моменты в UTC (Instant), а в местную зону переводите только при показе пользователю. Так вы избегаете ошибок на переходах летнего времени и между регионами.
ZonedDateTime msk = dt.atZone(ZoneId.of("Europe/Moscow"));
Instant instant = msk.toInstant(); // момент для хранения (UTC)
ZonedDateTime ny = instant.atZone(ZoneId.of("America/New_York")); // то же мгновение в другой зоне
Длительности: Duration и Period
Duration — машинная длительность в секундах и наносекундах (между Instant/LocalTime): «90 минут», «5 секунд». Period — календарная длительность в годах, месяцах и днях (между LocalDate): «2 месяца», «3 дня».
Разница не косметическая: Period.ofDays(1) и Duration.ofHours(24) могут дать разный результат на стыке перехода на летнее время, потому что календарный «день» не всегда равен 24 часам. Берите тип по смыслу: машинное время — Duration, календарь — Period.
Форматирование и разбор
Типы java.time умеют ISO-формат из коробки (toString → 2026-01-01T09:00). Для своих шаблонов есть DateTimeFormatter — и, в отличие от SimpleDateFormat, он неизменяемый и потокобезопасный, поэтому его держат в static-константе.
format(formatter) превращает дату в строку, parse(text, formatter) — строку в дату. Для локализованного вывода задают Locale; при разборе шаблон должен точно соответствовать входу, иначе будет DateTimeParseException.
static final DateTimeFormatter F = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
String s = dt.format(F); // "01.01.2026 09:00"
LocalDateTime back = LocalDateTime.parse(s, F);
Неизменяемость и совместимость с legacy
Все типы java.time неизменяемы: plusDays, withYear и т.п. возвращают НОВЫЙ объект, исходный не меняется — забытое присваивание (dt.plusDays(1) без сохранения) частая ошибка. Зато их безопасно шарить между потоками.
С legacy-кодом наводят мосты: new Date() ↔ Date.toInstant()/Date.from(Instant); java.sql-типы конвертируются своими методами. Новый код пишут на java.time, а конверсии делают только на границе со старым API.
LocalDate tomorrow = today.plusDays(1); // НОВЫЙ объект; today не изменился
Instant i = new java.util.Date().toInstant(); // мост из legacy
Как отвечать на собеседовании
Объясните, почему java.time вытеснил Date/Calendar (неизменяемость, потокобезопасность, явная модель, ловушки старого API). Различите Instant (машинный момент в UTC) и LocalDateTime (без зоны), ZonedDateTime/ZoneId и правило «хранить в UTC». Разведите Duration (секунды) и Period (календарь), упомяните потокобезопасный DateTimeFormatter против SimpleDateFormat и неизменяемость (методы возвращают новый объект).