Продвинутая работа с датами и временем в Java: от основ к профессионализму
Работа с датами и временем — одна из ключевых задач любого enterprise-приложения. В Java долгое время стандартом был громоздкий java.util.Date и java.util.Calendar, но с выходом Java 8 ситуация кардинально изменилась. Новый java.time API (JSR-310) предложил разработчикам мощный, потокобезопасный и интуитивно понятный инструментарий. В этой статье мы разберем продвинутые техники работы с датами и временем, которые пригодятся каждому Java-разработчику.
1. Основы java.time API: LocalDate, LocalTime, LocalDateTime
Первый шаг к профессиональной работе — понимание трех базовых классов, которые заменили старые аналоги:
- LocalDate — дата без времени (год, месяц, день).
- LocalTime — время без даты (часы, минуты, секунды, наносекунды).
- LocalDateTime — объединение даты и времени без привязки к часовому поясу.
// Получение текущей даты и времени
LocalDate today = LocalDate.now(); // 2025-03-21
LocalTime now = LocalTime.now(); // 14:30:15.123
LocalDateTime current = LocalDateTime.now(); // 2025-03-21T14:30:15.123
// Создание конкретной даты
LocalDate birthDate = LocalDate.of(1990, Month.JUNE, 15);
LocalTime meetingTime = LocalTime.of(10, 30, 0);
// Манипуляции с датами
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
Эти классы являются иммутабельными — любые операции возвращают новый объект. Это гарантирует потокобезопасность и предсказуемость кода.
2. Работа с часовыми поясами: ZonedDateTime и ZoneId
Глобальные приложения не могут игнорировать временные зоны. Для этого в java.time предусмотрен класс ZonedDateTime.
// Текущее время в UTC
ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
// Время в конкретном часовом поясе
ZoneId moscowZone = ZoneId.of("Europe/Moscow");
ZonedDateTime moscowTime = ZonedDateTime.now(moscowZone);
// Конвертация между зонами
ZonedDateTime nyTime = moscowTime.withZoneSameInstant(ZoneId.of("America/New_York"));
// Получение смещения
ZoneOffset offset = moscowTime.getOffset(); // +03:00
Совет профессионала: всегда храните даты в UTC на сервере и конвертируйте в локальный часовой пояс только на уровне представления (UI). Это избавит от проблем с переходом на летнее/зимнее время.
3. Парсинг и форматирование дат: DateTimeFormatter
Одна из самых частых задач — преобразование строки в дату и обратно. DateTimeFormatter предоставляет гибкие возможности.
Базовое форматирование
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse("21.03.2025 14:30:00", formatter);
String formatted = dateTime.format(formatter); // "21.03.2025 14:30:00"
Продвинутые шаблоны
// ISO-8601
DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
String isoDate = dateTime.format(isoFormatter); // "2025-03-21T14:30:00"
// Локализованное форматирование
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.ITALY);
String italianDate = dateTime.format(italianFormatter); // "21 marzo 2025"
// Свой формат с текстовыми полями
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR)
.appendLiteral(" год, ")
.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL)
.appendLiteral(" ")
.appendValue(ChronoField.DAY_OF_MONTH)
.toFormatter();
String custom = dateTime.format(customFormatter); // "2025 год, марта 21"
Важно помнить: DateTimeFormatter также потокобезопасен (в отличие от SimpleDateFormat), поэтому его можно объявлять как статическую константу.
4. Продвинутые вычисления: Duration, Period и ChronoUnit
Для расчета разницы между датами и временем используйте специализированные классы:
- Duration — для времени (секунды, наносекунды), подходит для
LocalTimeиLocalDateTime. - Period — для дат (годы, месяцы, дни), работает с
LocalDate. - ChronoUnit — универсальный перечислитель временных единиц.
// Разница между двумя датами
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2025, 3, 21);
Period period = Period.between(start, end);
System.out.println("Прошло: " + period.getYears() + " лет, "
+ period.getMonths() + " месяцев, " + period.getDays() + " дней");
// Разница в часах между двумя моментами
LocalDateTime startTime = LocalDateTime.of(2025, 3, 21, 10, 0);
LocalDateTime endTime = LocalDateTime.of(2025, 3, 21, 18, 30);
Duration duration = Duration.between(startTime, endTime);
long hours = duration.toHours(); // 8
long minutes = duration.toMinutes() % 60; // 30
// Использование ChronoUnit
long daysBetween = ChronoUnit.DAYS.between(start, end); // 445
long weeksBetween = ChronoUnit.WEEKS.between(start, end); // 63
Работа с интервалами и временными корректировщиками
// TemporalAdjuster — мощный инструмент для манипуляций
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
// Свой TemporalAdjuster — следующий рабочий день
TemporalAdjuster nextWorkDay = temporal -> {
LocalDate date = LocalDate.from(temporal);
do {
date = date.plusDays(1);
} while (date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY