$ sudo teach IT
МОДУЛЬ 11 · УРОК 2

Дата и время (java.time)

Работа с датами и временем — одна из самых частых задач в реальных приложениях. Логирование событий, расчёт сроков, работа с календарём, планировщики задач, анализ данных — всё это требует точного и удобного API для работы с датой и временем. В этом уроке мы разберём современный пакет java.time (Java 8+), который заменил старые Date и Calendar.

⏱ ~40 минут 🎓 Средний уровень ☕ Java

🕰️ Почему java.time?

До Java 8 для работы с датами использовались классы java.util.Date, java.util.Calendar и java.text.SimpleDateFormat. У них было множество проблем:

🔴
Date — изменяемый (mutability)

Date можно изменить после создания (setTime(), setYear()). Это приводит к багам в многопоточном коде. java.time классы — все неизменяемые и потокобезопасные.

🔴
Путаница с месяцами

В Calendar январь — это месяц 0, декабрь — 11. Легко ошибиться на единицу. В java.time месяцы — честные: январь — Month.JANUARY или значение 1.

🔴
SimpleDateFormat не потокобезопасен

SimpleDateFormat нельзя использовать из нескольких потоков без синхронизации. DateTimeFormatter — потокобезопасен, его можно сохранить в константу.

🔴
Нет ясного API

Date представляет и дату, и время одновременно. Нельзя сказать «мне нужна только дата без времени». java.time разделяет: LocalDate, LocalTime, LocalDateTime.

Разработчикам java.time вдохновлялись библиотекой Joda-Time (которая теперь практически не нужна — всё лучшее перешло в стандартную библиотеку). Автор Joda-Time, Стивен Коулборн, участвовал в разработке java.time.

💡 Ключевая идея: java.time разделяет концепции «дата», «время», «дата+время», «момент во времени» на разные классы. Каждый класс отвечает за свою область и делает это хорошо. Никаких больше универсальных Date, которые пытаются быть всем сразу.

📅 LocalDate — только дата

LocalDate представляет дату без времени и часового пояса: год, месяц, день. Используется для дней рождения, праздников, сроков, расписаний — всего, где время не важно.

import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;

public class LocalDateExample {
    public static void main(String[] args) {
        // Текущая дата
        LocalDate today = LocalDate.now();
        System.out.println("Сегодня: " + today);  // 2024-12-25

        // Создание конкретной даты
        LocalDate newYear = LocalDate.of(2025, Month.JANUARY, 1);
        LocalDate mayDay = LocalDate.of(2025, 5, 1);  // Month можно числом
        System.out.println("Новый год: " + newYear);

        // Парсинг из строки (ISO-8601: yyyy-MM-dd)
        LocalDate parsed = LocalDate.parse("2025-06-15");
        System.out.println("Спарсено: " + parsed);

        // Получение компонентов
        int year = today.getYear();
        int month = today.getMonthValue();   // 1-12 (не 0-11!)
        Month monthEnum = today.getMonth();   // Month.DECEMBER
        int day = today.getDayOfMonth();
        DayOfWeek weekday = today.getDayOfWeek();  // DayOfWeek.WEDNESDAY
        int dayOfYear = today.getDayOfYear();

        System.out.printf("Год: %d, Месяц: %d, День: %d, %s (день %d в году)%n",
            year, month, day, weekday, dayOfYear);
    }
}

📌 Манипуляции с датами (plus/minus)

LocalDate, как и все классы java.time, неизменяем. Методы plusDays, minusMonths и т.д. возвращают новые объекты, не меняя исходный.

public class DateManipulation {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();

        // Добавление/вычитание дней, месяцев, лет
        LocalDate tomorrow = today.plusDays(1);
        LocalDate nextWeek = today.plusWeeks(1);
        LocalDate nextMonth = today.plusMonths(1);
        LocalDate nextYear = today.plusYears(1);
        LocalDate lastWeek = today.minusWeeks(1);
        LocalDate lastYear = today.minusYears(5);

        // Специальные методы для настройки даты
        LocalDate firstDayOfMonth = today.withDayOfMonth(1);
        LocalDate lastDayOfMonth = today.withDayOfMonth(
            today.lengthOfMonth()  // количество дней в текущем месяце
        );
        LocalDate firstDayOfYear = today.withDayOfYear(1);

        // with(TemporalAdjuster) — мощный механизм
        LocalDate nextMonday = today.with(
            java.time.temporal.TemporalAdjusters.next(DayOfWeek.MONDAY)
        );
        LocalDate firstOfNextMonth = today.with(
            java.time.temporal.TemporalAdjusters.firstDayOfNextMonth()
        );

        System.out.println("Сегодня: " + today);
        System.out.println("Завтра: " + tomorrow);
        System.out.println("Следующий понедельник: " + nextMonday);
        System.out.println("Дней в этом месяце: " + today.lengthOfMonth());
        System.out.println("Високосный? " + today.isLeapYear());
    }
}

📌 Сравнение дат

public class DateComparison {
    public static void main(String[] args) {
        LocalDate d1 = LocalDate.of(2025, 6, 15);
        LocalDate d2 = LocalDate.of(2025, 12, 25);
        LocalDate d3 = LocalDate.of(2025, 6, 15);

        // Методы сравнения
        System.out.println(d1.isBefore(d2));   // true
        System.out.println(d2.isAfter(d1));    // true
        System.out.println(d1.isEqual(d3));    // true

        // Через compareTo (Comparable)
        System.out.println(d1.compareTo(d2));  // отрицательное (d1 раньше)
        System.out.println(d2.compareTo(d1));  // положительное
        System.out.println(d1.compareTo(d3));  // 0 — равны

        // equals
        System.out.println(d1.equals(d3));     // true
    }
}

💡 Аналогия: LocalDate — это как отрывной календарь. Вы можете посмотреть сегодняшнюю дату, оторвать листок (получить завтра), вернуться на неделю назад. Но сам календарь не меняется — каждый раз вы получаете новый листок.

⏰ LocalTime — только время

LocalTime представляет время без даты и часового пояса: часы, минуты, секунды, наносекунды. Используется для времени открытия магазина, начала урока, таймера.

import java.time.LocalTime;
import java.time.temporal.ChronoUnit;

public class LocalTimeExample {
    public static void main(String[] args) {
        // Текущее время
        LocalTime now = LocalTime.now();
        System.out.println("Сейчас: " + now);  // 14:30:25.123456789

        // Создание времени
        LocalTime noon = LocalTime.of(12, 0);          // 12:00
        LocalTime midnight = LocalTime.of(0, 0, 0);    // 00:00:00
        LocalTime precise = LocalTime.of(10, 30, 45, 500_000_000); // 10:30:45.500

        // Парсинг
        LocalTime parsed = LocalTime.parse("15:45:30");
        System.out.println("Парс: " + parsed);  // 15:45:30

        // Получение компонентов
        int hour = now.getHour();
        int minute = now.getMinute();
        int second = now.getSecond();
        int nano = now.getNano();

        // Манипуляции
        LocalTime inTwoHours = now.plusHours(2);
        LocalTime thirtyMinAgo = now.minusMinutes(30);
        LocalTime roundedToHour = now.truncatedTo(ChronoUnit.HOURS);

        // Сравнение
        System.out.println("Сейчас до полудня? " + now.isBefore(noon));
        System.out.println("Сейчас после полудня? " + now.isAfter(noon));
    }
}

💡 Важно: LocalTime может хранить время с точностью до наносекунд (9 знаков после запятой). Но не все часы в реальном мире так точны — системные часы обычно имеют точность до микросекунд или миллисекунд. ChronoUnit — перечисление единиц времени: HOURS, MINUTES, SECONDS, MILLIS, MICROS, NANOS и другие.

📆 LocalDateTime — дата и время вместе

LocalDateTime объединяет LocalDate и LocalTime. Это самый часто используемый класс для хранения момента во времени «без привязки к часовому поясу».

import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.LocalTime;

public class LocalDateTimeExample {
    public static void main(String[] args) {
        // Текущие дата и время
        LocalDateTime now = LocalDateTime.now();
        System.out.println("Сейчас: " + now);

        // Из отдельных компонентов
        LocalDateTime dt1 = LocalDateTime.of(2025, 6, 15, 14, 30);
        LocalDateTime dt2 = LocalDateTime.of(2025, 12, 25, 10, 0, 0);
        LocalDateTime dt3 = LocalDateTime.of(
            LocalDate.of(2025, 1, 1),
            LocalTime.of(0, 0, 0)
        );

        // Парсинг
        LocalDateTime parsed = LocalDateTime.parse("2025-06-15T14:30:00");
        System.out.println("Парс: " + parsed);

        // Разделение на дату и время
        LocalDate datePart = parsed.toLocalDate();
        LocalTime timePart = parsed.toLocalTime();
        System.out.println("Дата: " + datePart + ", Время: " + timePart);

        // Манипуляции
        LocalDateTime nextHour = now.plusHours(1);
        LocalDateTime nextWeek = now.plusWeeks(1);
        LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();  // 00:00
    }
}

⚠️ Важно понимать: LocalDateTime не хранит информацию о часовом поясе. Если сейчас 14:30 в Москве и 14:30 во Владивостоке — это разные моменты времени, но LocalDateTime не сможет их различить. Для работы с часовыми поясами есть ZonedDateTime и OffsetDateTime.

🎨 DateTimeFormatter — форматирование и парсинг

DateTimeFormatter — потокобезопасный класс для форматирования дат в строки и парсинга строк в даты. Его можно сохранять в статические константы и использовать из разных потоков без синхронизации.

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class DateTimeFormatterExample {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        // Предопределённые форматеры
        System.out.println("ISO_LOCAL_DATE: " + now.format(DateTimeFormatter.ISO_LOCAL_DATE));
        System.out.println("ISO_LOCAL_TIME: " + now.format(DateTimeFormatter.ISO_LOCAL_TIME));
        System.out.println("ISO_LOCAL_DATE_TIME: " + now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        // ISO: 2025-06-15T14:30:00

        // Форматы с локалью
        System.out.println("SHORT: " + now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));
        System.out.println("MEDIUM: " + now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)));
        System.out.println("LONG: " + now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)));
        System.out.println("FULL: " + now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)));
        // Примерно: 15.06.2025, 15 июн. 2025 г., 15 июня 2025 г.

        // Свой шаблон
        DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
        System.out.println("Кастом: " + now.format(custom));  // 15.06.2025 14:30:00

        DateTimeFormatter russian = DateTimeFormatter.ofPattern(
            "EEEE, d MMMM yyyy 'г.' HH:mm", new Locale("ru")
        );
        System.out.println("По-русски: " + now.format(russian));
        // воскресенье, 15 июня 2025 г. 14:30

        // Парсинг строки обратно в дату
        String dateStr = "25.12.2025";
        DateTimeFormatter parser = DateTimeFormatter.ofPattern("dd.MM.yyyy");
        LocalDate parsed = LocalDate.parse(dateStr, parser);
        System.out.println("Спарсено: " + parsed);  // 2025-12-25

        // Безопасность: если строка не соответствует формату — DateTimeParseException
        try {
            LocalDate.parse("32.13.2025", parser);
        } catch (java.time.format.DateTimeParseException e) {
            System.out.println("Ошибка парсинга: " + e.getMessage());
        }
    }
}
Символ Значение Пример
y год 2025
M месяц (число или текст) 6, 06, Jun, June
d день месяца 15, 05
E день недели (текст) пн, понедельник
H час (0-23) 14
h час (1-12 AM/PM) 2
m минута 30
s секунда 45
a AM/PM маркер PM

⏳ Duration и Period — разница во времени

Для вычисления разницы между датами/временем есть два класса: Duration (для времени — часы, минуты, секунды, наносекунды) и Period (для дат — годы, месяцы, дни).

import java.time.*;
import java.time.temporal.ChronoUnit;

public class DurationPeriodExample {
    public static void main(String[] args) {
        // Duration — разница во времени (наносекунды)
        LocalTime start = LocalTime.of(9, 0);
        LocalTime end = LocalTime.of(17, 30);
        Duration workDay = Duration.between(start, end);

        System.out.println("Рабочий день:");
        System.out.println("  часов: " + workDay.toHours());        // 8
        System.out.println("  минут: " + workDay.toMinutes());      // 510
        System.out.println("  секунд: " + workDay.toSeconds());     // 30600
        System.out.println("  в ISO: " + workDay);                  // PT8H30M

        // Duration из чисел
        Duration twoHours = Duration.ofHours(2);
        Duration thirtyMin = Duration.ofMinutes(30);
        Duration oneDay = Duration.of(1, ChronoUnit.DAYS);

        // Period — разница между датами
        LocalDate startDate = LocalDate.of(2020, 1, 1);
        LocalDate endDate = LocalDate.of(2025, 6, 15);
        Period period = Period.between(startDate, endDate);

        System.out.println("Период:");
        System.out.println("  лет: " + period.getYears());      // 5
        System.out.println("  месяцев: " + period.getMonths()); // 5
        System.out.println("  дней: " + period.getDays());      // 14
        System.out.println("  всего месяцев: " + period.toTotalMonths()); // 65
        System.out.println("  в ISO: " + period);               // P5Y5M14D

        // ChronoUnit.between — общее количество единиц
        long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
        long monthsBetween = ChronoUnit.MONTHS.between(startDate, endDate);
        long weeksBetween = ChronoUnit.WEEKS.between(startDate, endDate);
        System.out.println("Между датами:");
        System.out.println("  дней: " + daysBetween);
        System.out.println("  месяцев: " + monthsBetween);
        System.out.println("  недель: " + weeksBetween);
    }
}

💡 Разница между Duration и Period простыми словами: Duration измеряет время в секундах/наносекундах — это как секундомер. Period измеряет в днях/месяцах/годах — это как календарь. Duration может сказать, сколько секунд между 9:00 и 17:00. Period скажет, сколько лет и месяцев между 1 января 2020 и 15 июня 2025. ChronoUnit.between — универсальный метод, который работает и с датами, и с временем, возвращая количество в указанных единицах.

🌍 ZonedDateTime — дата и время с часовым поясом

Когда нужно хранить момент во времени с учётом часового пояса (например, время встречи для международных участников), используйте ZonedDateTime или OffsetDateTime. ZoneId представляет часовой пояс.

import java.time.*;

public class ZonedDateTimeExample {
    public static void main(String[] args) {
        // Все доступные ID часовых поясов
        // System.out.println(ZoneId.getAvailableZoneIds());  // ~600 поясов

        // Текущее время в конкретном поясе
        ZonedDateTime moscowNow = ZonedDateTime.now(ZoneId.of("Europe/Moscow"));
        ZonedDateTime newYorkNow = ZonedDateTime.now(ZoneId.of("America/New_York"));
        ZonedDateTime tokyoNow = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

        System.out.println("Москва: " + moscowNow);
        System.out.println("Нью-Йорк: " + newYorkNow);
        System.out.println("Токио: " + tokyoNow);

        // Конвертация между поясами
        ZonedDateTime meetingInMoscow = ZonedDateTime.of(
            LocalDate.of(2025, 6, 20),
            LocalTime.of(15, 0),
            ZoneId.of("Europe/Moscow")
        );
        ZonedDateTime meetingInNY = meetingInMoscow.withZoneSameInstant(
            ZoneId.of("America/New_York")
        );
        System.out.println("Встреча в Москве: " + meetingInMoscow);
        System.out.println("Встреча в Нью-Йорке: " + meetingInNY);
        // Москва 15:00 -> Нью-Йорк 08:00 (летом разница -7 часов)

        // Instant — момент в UTC (машинное время)
        Instant now = Instant.now();
        System.out.println("Instant (UTC): " + now);
        // Instant — это количество секунд/наносекунд с 1970-01-01T00:00:00Z

        // Instant в LocalDateTime с указанием пояса
        LocalDateTime moscowTime = LocalDateTime.ofInstant(
            now, ZoneId.of("Europe/Moscow")
        );
        System.out.println("Instant как Московское: " + moscowTime);
    }
}

🔑 Правильный выбор класса: Если вам нужно записать «когда это произошло» в глобальном смысле — используйте Instant (UTC). Если нужно локальное время события (например, «концерт в 19:00 по местному времени») — используйте LocalDateTime в паре с ZoneId. ZonedDateTime сочетает и то, и другое, но тяжелее.

⚠️ Старый Date и Calendar (НЕ РЕКОМЕНДУЕТСЯ)

Вы обязательно встретите старый код с java.util.Date и java.util.Calendar. Нужно уметь их читать и конвертировать в java.time. Но писать новый код с ними — плохая практика.

import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.time.*;

public class OldApiDemo {
    public static void main(String[] args) {
        // Date — устаревший, но всё ещё часто встречается
        Date now = new Date();                 // текущий момент
        Date specific = new Date(125, 5, 15);  // Deprecated! 2025-06-15
        // Годы: 1900-based, месяцы: 0-based — постоянные ошибки!

        // Calendar — немного лучше, но тоже не подарок
        Calendar cal = Calendar.getInstance();
        cal.set(2025, Calendar.JUNE, 15, 14, 30, 0);
        Date meeting = cal.getTime();
        System.out.println("Calendar: " + meeting);

        // Конвертация старого API в java.time
        // Date -> Instant
        Instant instant = now.toInstant();
        System.out.println("Instant: " + instant);

        // Instant -> LocalDateTime (с указанием пояса)
        LocalDateTime ldt = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
        System.out.println("LocalDateTime: " + ldt);

        // Calendar -> ZonedDateTime
        ZonedDateTime zdt = ((GregorianCalendar) cal).toZonedDateTime();
        System.out.println("ZonedDateTime: " + zdt);

        // java.time -> Date
        Instant backToInstant = ldt.atZone(ZoneId.systemDefault()).toInstant();
        Date backToDate = Date.from(backToInstant);
        System.out.println("Date обратно: " + backToDate);
    }
}

🚫 Почему новый код нельзя писать на Date/Calendar? (1) Месяцы с 0 — январь = 0, декабрь = 11. (2) Мутабельность — Date и Calendar можно изменить после создания. (3) Отсутствует ясность — Date — это и дата, и время. (4) SimpleDateFormat не потокобезопасен. (5) Нет поддержки Period, Duration, TemporalAdjusters. Используйте java.time — это современно, безопасно и удобно.

🏋️ Практические примеры

📌 Пример 1: Возраст по дате рождения

import java.time.*;
import java.time.temporal.ChronoUnit;

public class AgeCalculator {
    public static int calculateAge(LocalDate birthDate) {
        LocalDate today = LocalDate.now();
        return (int) ChronoUnit.YEARS.between(birthDate, today);
    }

    public static String detailedAge(LocalDate birthDate) {
        LocalDate today = LocalDate.now();
        Period period = Period.between(birthDate, today);
        return String.format("%d лет, %d месяцев, %d дней",
            period.getYears(), period.getMonths(), period.getDays());
    }

    public static void main(String[] args) {
        LocalDate birth = LocalDate.of(1990, 5, 15);
        System.out.println("Возраст: " + calculateAge(birth) + " лет");
        System.out.println("Детально: " + detailedAge(birth));
    }
}

📌 Пример 2: Количество рабочих дней между датами

import java.time.*;
import java.time.temporal.ChronoUnit;

public class WorkingDaysCalculator {
    public static long countWorkingDays(LocalDate start, LocalDate end) {
        long workingDays = 0;
        LocalDate current = start;

        while (!current.isAfter(end)) {
            DayOfWeek day = current.getDayOfWeek();
            if (day != DayOfWeek.SATURDAY && day != DayOfWeek.SUNDAY) {
                workingDays++;
            }
            current = current.plusDays(1);
        }
        return workingDays;
    }

    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2025, 6, 1);
        LocalDate end = LocalDate.of(2025, 6, 30);
        System.out.println("Рабочих дней в июне: " + countWorkingDays(start, end));
    }
}

📌 Пример 3: Планировщик напоминаний

import java.time.*;
import java.time.temporal.TemporalAdjusters;

public class ReminderScheduler {
    public static void printReminders() {
        LocalDate today = LocalDate.now();

        System.out.println("=== Напоминания ===");
        System.out.println("Сегодня: " + today);

        // Последний день месяца — оплата счетов
        LocalDate payDay = today.with(
            TemporalAdjusters.lastDayOfMonth()
        );
        System.out.println("💳 Оплата счетов: " + payDay);

        // Следующий понедельник — встреча
        LocalDate nextMonday = today.with(
            TemporalAdjusters.next(DayOfWeek.MONDAY)
        );
        System.out.println("📅 Встреча: " + nextMonday);

        // Первое число следующего месяца — отчёт
        LocalDate reportDay = today.with(
            TemporalAdjusters.firstDayOfNextMonth()
        );
        System.out.println("📊 Отчёт: " + reportDay);

        // Разница в днях
        long daysToReport = ChronoUnit.DAYS.between(today, reportDay);
        System.out.println("До отчёта: " + daysToReport + " дней");
    }

    public static void main(String[] args) {
        printReminders();
    }
}

📌 Пример 4: Логирование с временными метками

import java.time.*;
import java.time.format.DateTimeFormatter;

public class Logger {
    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss.SSS");

    public static void log(String level, String message) {
        String timestamp = LocalDateTime.now().format(FORMATTER);
        System.out.printf("[%s] %s: %s%n", timestamp, level, message);
    }

    public static void main(String[] args) {
        log("INFO", "Приложение запущено");
        log("WARN", "Использование большого количества памяти");
        log("ERROR", "Ошибка подключения к базе данных");
    }
}

✅ Итоги урока

  • java.time — современный API для работы с датой и временем (Java 8+). Неизменяемые, потокобезопасные классы
  • LocalDate — дата без времени (год, месяц, день). Методы: now, of, parse, plusDays, minusMonths, withDayOfMonth, isBefore, isLeapYear
  • LocalTime — время без даты (часы, минуты, секунды, наносекунды). Методы: now, of, parse, plusHours, minusMinutes, isAfter
  • LocalDateTime — дата и время вместе. Создаётся через LocalDateTime.of(date, time) или LocalDateTime.now()
  • DateTimeFormatter — потокобезопасный форматтер. Предопределённые (ISO_LOCAL_DATE) и кастомные шаблоны (ofPattern)
  • Duration — разница во времени (часы, минуты, секунды). between(start, end), toHours, toMinutes
  • Period — разница между датами (годы, месяцы, дни). between(start, end), getYears, getMonths, toTotalMonths
  • ChronoUnit — универсальные единицы: DAYS.between, MONTHS.between, WEEKS.between
  • ZonedDateTime — дата+время+часовой пояс. ZoneId, withZoneSameInstant
  • Instant — момент в UTC (машинное время). Незаменим для хранения меток времени в базах данных
  • Старые Date/Calendar — не используйте в новом коде. При необходимости конвертируйте через toInstant(), Date.from()

В следующем уроке разберём работу с файлами — чтение, запись, создание и удаление через java.nio.file. 🚀

Тест: Дата и время (java.time)

10 вопросов

Форматирование даты

Premium