☕ Java

Работа с датой и временем в Java: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы ведёте дневник. Каждый день вы записываете: «Сегодня 15 марта, 14:30. Я сделал домашнее задание». Без даты и времени ваши записи превратились бы в кашу — вы бы не знали, когда что произошло. В программировании то же самое: компьютеру нужно точно знать, когда случилось событие.

Давайте представим, что ваша программа — это умный будильник. Он должен:

  • Знать, который сейчас час (чтобы разбудить вас в 7:00)
  • Помнить, когда вы ложитесь спать (чтобы не звонить ночью)
  • Понимать, что 31 февраля не существует (чтобы не сломаться)
  • Уметь считать, сколько дней осталось до Нового года

В реальной жизни мы используем календари и часы. В Java для этого есть специальные «умные часы» — классы для работы с датой и временем. Они умеют всё то же самое, что и обычные часы, но ещё и могут:

  • Сравнивать даты (что было раньше?)
  • Вычислять разницу между датами (сколько дней прошло?)
  • Форматировать дату (показать по-русски «15 марта 2024» или по-английски «March 15, 2024»)
  • Проверять, является ли год високосным
💡 Важно: В старых версиях Java (до 8-й) работа с датой была очень неудобной. Сейчас мы используем современный API — java.time. Он появился в Java 8 (2014 год) и считается лучшим способом работы с датой и временем.

Теория: как это работает изнутри

Компьютер не понимает «15 марта» так, как понимаем мы. Для него время — это просто огромное число. Это число — количество миллисекунд, прошедших с 1 января 1970 года 00:00:00 UTC. Эта дата называется Unix-эпоха (или Epoch).

Почему именно 1970 год? Потому что тогда создали операционную систему Unix, и разработчики решили: «Пусть время считается от этой даты». С тех пор все компьютеры в мире используют эту точку отсчёта.

Давайте посмотрим, как это работает:

  • 1 января 1970, 00:00:00 — это 0 миллисекунд
  • 1 января 1970, 00:00:01 — это 1000 миллисекунд
  • 1 января 2024, 00:00:00 — это примерно 1 704 067 200 000 миллисекунд

Когда вы пишете в коде LocalDate.of(2024, 3, 15), Java внутри делает примерно следующее:

  1. Проверяет, существует ли такая дата (нет ли 30 февраля)
  2. Вычисляет количество дней от 1970 года до этой даты
  3. Сохраняет это число в памяти

В Java 8+ есть несколько основных классов для работы с датой и временем. Они находятся в пакете java.time:

Класс Что хранит Пример
LocalDate Только дату (год, месяц, день) 2024-03-15
LocalTime Только время (часы, минуты, секунды) 14:30:00
LocalDateTime Дату и время вместе 2024-03-15T14:30:00
ZonedDateTime Дату, время и часовой пояс 2024-03-15T14:30:00+03:00
Instant Момент времени (для машин) 2024-03-15T11:30:00Z
Duration Разница во времени (часы, минуты) PT2H30M (2 часа 30 минут)
Period Разница в датах (годы, месяцы, дни) P1Y2M3D (1 год 2 месяца 3 дня)
⚠️ Важное замечание: Все классы из java.time — иммутабельные (неизменяемые). Это значит, что вы не можете изменить существующий объект с датой. Вместо этого вы создаёте новый объект с изменёнными значениями. Это сделано для безопасности — чтобы случайно не «сломать» дату в другом месте программы.

Синтаксис в Java: все формы и варианты

Давайте разберём все основные способы работы с датой и временем. Мы будем использовать современный API java.time.

1. Создание даты и времени

Есть несколько способов создать объект с датой:

import java.time.*;

public class CreateDates {
    public static void main(String[] args) {
        // Способ 1: Текущая дата и время
        LocalDate today = LocalDate.now();           // 2024-03-15
        LocalTime now = LocalTime.now();             // 14:30:00.123
        LocalDateTime current = LocalDateTime.now(); // 2024-03-15T14:30:00.123
        
        // Способ 2: Указать конкретную дату
        LocalDate birthday = LocalDate.of(2010, 5, 20);      // 20 мая 2010
        LocalTime alarm = LocalTime.of(7, 30);               // 07:30
        LocalDateTime meeting = LocalDateTime.of(2024, 12, 31, 23, 59);
        
        // Способ 3: Из строки
        LocalDate parsed = LocalDate.parse("2024-03-15");    // ГГГГ-ММ-ДД
        LocalTime parsedTime = LocalTime.parse("14:30:00");  // ЧЧ:ММ:СС
        
        // Способ 4: С часовым поясом
        ZonedDateTime moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"));
        ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
    }
}

2. Получение частей даты

Из даты можно «вытащить» отдельные части:

LocalDate date = LocalDate.of(2024, 3, 15);

int year = date.getYear();          // 2024
int month = date.getMonthValue();   // 3 (число)
Month monthEnum = date.getMonth();  // MARCH (перечисление)
int day = date.getDayOfMonth();     // 15
int dayOfYear = date.getDayOfYear(); // 75 (15 марта — 75-й день года)
DayOfWeek dayOfWeek = date.getDayOfWeek(); // FRIDAY (пятница)

// Для времени:
LocalTime time = LocalTime.of(14, 30, 45);
int hour = time.getHour();          // 14
int minute = time.getMinute();      // 30
int second = time.getSecond();      // 45
int nano = time.getNano();          // 0 (наносекунды)

3. Изменение даты

Помните, что даты неизменяемы? Поэтому мы используем методы, которые возвращают новый объект:

LocalDate date = LocalDate.of(2024, 3, 15);

// Добавление дней, месяцев, лет
LocalDate nextWeek = date.plusDays(7);      // 2024-03-22
LocalDate nextMonth = date.plusMonths(1);   // 2024-04-15
LocalDate nextYear = date.plusYears(1);     // 2025-03-15

// Вычитание
LocalDate yesterday = date.minusDays(1);    // 2024-03-14
LocalDate lastMonth = date.minusMonths(1);  // 2024-02-15

// Изменение конкретных полей
LocalDate firstOfMonth = date.withDayOfMonth(1);     // 2024-03-01
LocalDate lastOfYear = date.withMonth(12);            // 2024-12-15
LocalDate newYear = date.withDayOfYear(1);            // 2024-01-01

4. Сравнение дат

Даты можно сравнивать между собой:

LocalDate date1 = LocalDate.of(2024, 3, 15);
LocalDate date2 = LocalDate.of(2024, 12, 31);

boolean isBefore = date1.isBefore(date2);  // true (15 марта раньше 31 декабря)
boolean isAfter = date1.isAfter(date2);    // false
boolean isEqual = date1.isEqual(date2);    // false

// Можно также использовать compareTo()
int comparison = date1.compareTo(date2);   // отрицательное число (date1 < date2)

5. Разница между датами

Для вычисления разницы используем Period (для дат) и Duration (для времени):

LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);

Period period = Period.between(start, end);
int years = period.getYears();     // 0
int months = period.getMonths();   // 11
int days = period.getDays();       // 30

// Для точной разницы в днях:
long daysBetween = ChronoUnit.DAYS.between(start, end);  // 365

// Для времени:
LocalTime startTime = LocalTime.of(10, 0);
LocalTime endTime = LocalTime.of(14, 30);
Duration duration = Duration.between(startTime, endTime);
long hours = duration.toHours();      // 4
long minutes = duration.toMinutes();  // 270

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

Чтобы показать дату в понятном для человека виде, используем DateTimeFormatter:

import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

LocalDate date = LocalDate.of(2024, 3, 15);

// Встроенные форматы
String iso = date.format(DateTimeFormatter.ISO_DATE);           // 2024-03-15
String full = date.format(DateTimeFormatter.ofLocalizedDate(
    FormatStyle.FULL));   // 15 марта 2024 г. (зависит от локали)
String medium = date.format(DateTimeFormatter.ofLocalizedDate(
    FormatStyle.MEDIUM)); // 15.03.2024

// Свой формат
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy");
String formatted = date.format(myFormat);  // 15.03.2024

// Другие примеры паттернов:
// "dd MMMM yyyy" — 15 марта 2024
// "yyyy/MM/dd" — 2024/03/15
// "EEEE, dd MMMM" — пятница, 15 марта

7. Работа с часовыми поясами

// Все доступные часовые пояса
Set allZones = ZoneId.getAvailableZoneIds();

// Создание даты с часовым поясом
ZonedDateTime moscow = ZonedDateTime.now(ZoneId.of("Europe/Moscow"));
ZonedDateTime london = ZonedDateTime.now(ZoneId.of("Europe/London"));

// Конвертация между поясами
ZonedDateTime moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"));
ZonedDateTime newYorkTime = moscowTime.withZoneSameInstant(
    ZoneId.of("America/New_York"));

// Проверка, является ли дата летним временем
boolean isDST = moscow.getZone().getRules().isDaylightSavings(
    moscow.toInstant());

Примеры кода — от простого к сложному

Пример 1: Сколько дней до дня рождения?

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

public class DaysUntilBirthday {
    public static void main(String[] args) {
        // Запрашиваем дату рождения пользователя
        LocalDate birthday = LocalDate.of(2010, 5, 20);
        LocalDate today = LocalDate.now();
        
        // Берём день рождения в этом году
        LocalDate nextBirthday = birthday.withYear(today.getYear());
        
        // Если день рождения уже был в этом году, берём следующий год
        if (nextBirthday.isBefore(today) || nextBirthday.isEqual(today)) {
            nextBirthday = nextBirthday.plusYears(1);
        }
        
        // Считаем количество дней
        long daysUntil = ChronoUnit.DAYS.between(today, nextBirthday);
        
        System.out.println("До твоего дня рождения осталось " + daysUntil + " дней!");
        System.out.println("Твой день рождения: " + nextBirthday);
    }
}

Пример 2: Проверка, является ли год високосным

import java.time.Year;

public class LeapYearChecker {
    public static void main(String[] args) {
        int year = 2024;
        
        // Способ 1: через Year
        Year y = Year.of(year);
        boolean isLeap1 = y.isLeap();
        
        // Способ 2: через LocalDate
        boolean isLeap2 = LocalDate.of(year, 1, 1).isLeapYear();
        
        System.out.println(year + " год високосный? " + isLeap1);
        
        // Проверим несколько лет
        int[] years = {1900, 2000, 2024, 2100};
        for (int y2 : years) {
            System.out.println(y2 + " — " + 
                (Year.of(y2).isLeap() ? "високосный" : "не високосный"));
        }
    }
}

Пример 3: Таймер обратного отсчёта до Нового года

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

public class NewYearCountdown {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        
        // Новый год следующего года
        LocalDateTime newYear = LocalDateTime.of(
            now.getYear() + 1, 1, 1, 0, 0, 0
        );
        
        // Разница в секундах
        long totalSeconds = ChronoUnit.SECONDS.between(now, newYear);
        
        // Переводим в дни, часы, минуты, секунды
        long days = totalSeconds / (24 * 3600);
        long hours = (totalSeconds % (24 * 3600)) / 3600;
        long minutes = (totalSeconds % 3600) / 60;
        long seconds = totalSeconds % 60;
        
        System.out.println("До Нового года осталось:");
        System.out.println(days + " дней, " + hours + " часов, " 
            + minutes + " минут, " + seconds + " секунд");
        
        // Альтернативный способ через Duration
        Duration duration = Duration.between(now, newYear);
        System.out.println("Или " + duration.toDays() + " дней");
        System.out.println("Или " + duration.toHours() + " часов");
    }
}

Пример 4: Календарь на месяц

import java.time.*;
import java.time.format.TextStyle;
import java.util.Locale;

public class MonthCalendar {
    public static void main(String[] args) {
        // Создаём календарь на март 2024
        int year = 2024;
        int month = 3;
        
        LocalDate firstDay = LocalDate.of(year, month, 1);
        LocalDate lastDay = firstDay.withDayOfMonth(
            firstDay.lengthOfMonth());
        
        // Название месяца и года
        String monthName = firstDay.getMonth()
            .getDisplayName(TextStyle.FULL, Locale.forLanguageTag("ru"));
        System.out.println("    " + monthName + " " + year);
        System.out.println("Пн Вт Ср Чт Пт Сб Вс");
        
        // Определяем день недели первого числа
        // Понедельник = 1, Воскресенье = 7
        int firstDayOfWeek = firstDay.getDayOfWeek().getValue();
        
        // Печатаем отступы для первой недели
        for (int i = 1; i < firstDayOfWeek; i++) {
            System.out.print("   ");
        }
        
        // Печатаем все дни месяца
        for (int day = 1; day <= lastDay.getDayOfMonth(); day++) {
            System.out.printf("%2d ", day);
            
            // Переход на новую строку после воскресенья
            LocalDate currentDate = LocalDate.of(year, month, day);
            if (currentDate.getDayOfWeek().getValue() == 7) {
                System.out.println();
            }
        }
    }
}

Пример 5: Парсинг даты из разных форматов

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

public class DateParser {
    public static void main(String[] args) {
        // Разные форматы дат, которые могут прийти от пользователя
        String[] dateStrings = {
            "2024-03-15",           // ISO формат
            "15.03.2024",           // Российский формат
            "15/03/2024",           // Европейский формат
            "March 15, 2024",       // Американский формат
            "2024-03-15T14:30:00"   // Дата и время
        };
        
        for (String dateStr : dateStrings) {
            try {
                // Пробуем распарсить ISO формат (самый простой)
                if (dateStr.contains("T")) {
                    LocalDateTime dateTime = LocalDateTime.parse(dateStr);
                    System.out.println("Распарсено: " + dateTime);
                } else if (dateStr.contains("-") && !dateStr.contains("/")) {
                    LocalDate date = LocalDate.parse(dateStr);
                    System.out.println("Распарсено: " + date);
                } else {
                    // Создаём свой формат
                    DateTimeFormatter formatter = DateTimeFormatter
                        .ofPattern("dd.MM.yyyy");
                    LocalDate date = LocalDate.parse(dateStr, formatter);
                    System.out.println("Распарсено: " + date);
                }
            } catch (DateTimeParseException e) {
                System.out.println("Ошибка парсинга: " + dateStr);
            }
        }
    }
}

Пример 6: Возраст человека в днях

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

public class AgeInDays {
    public static void main(String[] args) {
        // Дата рождения
        LocalDate birthDate = LocalDate.of(2010, 5, 20);
        LocalDate today = LocalDate.now();
        
        // Возраст в годах, месяцах, днях
        Period age = Period.between(birthDate, today);
        System.out.println("Возраст: " + age.getYears() + " лет, " 
            + age.getMonths() + " месяцев, " + age.getDays() + " дней");
        
        // Возраст в днях
        long daysOld = ChronoUnit.DAYS.between(birthDate, today);
        System.out.println("Или " + daysOld + " дней");
        
        // Возраст в часах
        long hoursOld = ChronoUnit.HOURS.between(
            birthDate.atStartOfDay(), today.atStartOfDay());
        System.out.println("Или " + hoursOld + " часов");
        
        // Сколько дней вы уже прожили в процентах от года
        double percentOfYear = (daysOld % 365) / 3.65;
        System.out.printf("Прожито %.1f%% от текущего года%n", percentOfYear);
    }
}

Пример 7: Конвертер времени между часовыми поясами

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

public class TimeZoneConverter {
    public static void main(String[] args) {
        // Создаём время в Москве
        ZonedDateTime moscowTime = ZonedDateTime.of(
            2024, 3, 15, 14, 30, 0, 0,
            ZoneId.of("Europe/Moscow")
        );
        
        System.out.println("Москва: " + moscowTime);
        
        // Конвертируем в другие города
        ZonedDateTime londonTime = moscowTime
            .withZoneSameInstant(ZoneId.of("Europe/London"));
        ZonedDateTime newYorkTime = moscowTime
            .withZoneSameInstant(ZoneId.of("America/New_York"));
        ZonedDateTime tokyoTime = moscowTime
            .withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
        ZonedDateTime sydneyTime = moscowTime
            .withZoneSameInstant(ZoneId.of("Australia/Sydney"));
        
        // Форматируем для красивого вывода
        DateTimeFormatter formatter = DateTimeFormatter
            .ofPattern("dd.MM.yyyy HH:mm");
        
        System.out.println("Лондон: " + londonTime.format(formatter));
        System.out.println("Нью-Йорк: " + newYorkTime.format(formatter));
        System.out.println("Токио: " + tokyoTime.format(formatter));
        System.out.println("Сидней: " + sydneyTime.format(formatter));
        
        // Показываем разницу в часах
        System.out.println("
Разница с Москвой:");
        System.out.println("Лондон: " + 
            ChronoUnit.HOURS.between(moscowTime, londonTime) + " часов");
        System.out.println("Нью-Йорк: " + 
            ChronoUnit.HOURS.between(moscowTime, newYorkTime) + " часов");
    }
}

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python, вам будет легче понять Java. Давайте сравним:

Операция Java (java.time) Python (datetime)
Текущая дата LocalDate.now() date.today()
Текущее время LocalTime.now() datetime.now().time()
Создать дату LocalDate.of(2024, 3, 15) date(2024, 3, 15)
Разница в днях ChronoUnit.DAYS.between(d1, d2) (d2 - d1).days
Форматирование DateTimeFormatter.ofPattern("dd.MM.yyyy") strftime("%d.%m.%Y")
Парсинг строки LocalDate.parse("2024-03-15") datetime.strptime("2024-03-15", "%Y-%m-%d")
Изменяемость Неизменяемые (immutable) Неизменяемые (immutable)

Основные отличия:

  • Синтаксис: В Java методы называются через точку и используют of, now, between. В Python — функции и операторы.
  • Типы: В Java разные классы для даты, времени и даты-времени. В Python — один класс datetime.
  • Часовые пояса: В Java нужно явно указывать ZoneId. В Python — pytz или zoneinfo.
  • Производительность: Java обычно быстрее работает с датами, особенно в больших объёмах.

Типичные ошибки и подводные камни

Ошибка 1: Использование старых классов

Многие учебники и старые статьи учат использовать java.util.Date и java.util.Calendar. Не делайте так! Эти классы устарели, неудобны и содержат ошибки в проектировании.

// ❌ ПЛОХО (старый способ)
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.set(2024, Calendar.MARCH, 15); // Месяцы считаются с 0!

// ✅ ХОРОШО (современный способ)
LocalDate date = LocalDate.of(2024, 3, 15);

Ошибка 2: Забыли про неизменяемость

Помните, что LocalDate нельзя изменить. Методы plusDays(), withMonth() и другие возвращают новый объект.

// ❌ ПЛОХО
LocalDate date = LocalDate.of(2024, 3, 15);
date.plusDays(7); // Результат никуда не сохранён!
System.out.println(date); // Всё ещё 2024-03-15

// ✅ ХОРОШО
LocalDate date = LocalDate.of(2024, 3, 15);
LocalDate nextWeek = date.plusDays(7);
System.out.println(nextWeek); // 2024-03-22

Ошибка 3: Неправильный формат строки

При парсинге даты из строки формат должен точно соответствовать. Иначе — ошибка.

// ❌ ПЛОХО
LocalDate date = LocalDate.parse("15.03.2024"); // Ошибка! Ожидается 2024-03-15

// ✅ ХОРОШО
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
LocalDate date = LocalDate.parse("15.03.2024", formatter);

Ошибка 4: Путаница между Period и Duration

Period — для дат (годы, месяцы, дни). Duration — для времени (часы, минуты, секунды). Не путайте!

// ❌ ПЛОХО
Duration.between(
    LocalDate.of(2024, 1, 1), 
    LocalDate.of(2024, 12, 31)
); // Ошибка компиляции!

// ✅ ХОРОШО
Period.between(
    LocalDate.of(2024, 1, 1), 
    LocalDate.of(2024, 12, 31)
);

Ошибка 5: Игнорирование часовых поясов

Если ваша программа работает с пользователями из разных стран, всегда используйте ZonedDateTime.

// ❌ ПЛОХО (для международного приложения)
LocalDateTime now = LocalDateTime.now(); // Неизвестно, какой часовой пояс

// ✅ ХОРОШО
ZonedDateTime now = ZonedDateTime.now(); // Учитывает часовой пояс сервера
// Или для конкретного пользователя:
ZonedDateTime userTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

Лучшие практики и советы профессионалов

  1. Всегда используйте java.time. Никогда не используйте java.util.Date или java.util.Calendar в новом коде.
  2. Храните даты в UTC. В базах данных и при передаче между системами храните время в UTC. Конвертируйте в локальное время только для отображения пользователю.
  3. Используйте Instant для машинного времени. Если вам нужно просто зафиксировать момент (например, время создания записи), используйте Instant — он быстрее и проще.
  4. Будьте осторожны с LocalDateTime. Он не содержит информации о часовом поясе. Используйте его только если часовой пояс не важен (например, день рождения).
  5. Проверяйте даты на валидность. Всегда проверяйте, что пользователь ввёл корректную дату, особенно если дата вводится вручную.
  6. Используйте DateTimeFormatter с локалью. Для отображения даты пользователю учитывайте его локаль (язык, страну).
  7. Не создавайте DateTimeFormatter каждый раз. Создайте его один раз как константу и переиспользуйте.
// Пример хорошей практики
public class DateUtils {
    // Константы форматтеров
    private static final DateTimeFormatter ISO_FORMATTER = 
        DateTimeFormatter.ISO_DATE;
    private static final DateTimeFormatter RUSSIAN_FORMATTER = 
        DateTimeFormatter.ofPattern("dd.MM.yyyy");
    
    // Метод для безопасного парсинга
    public static LocalDate parseDate(String dateStr) {
        try {
            return LocalDate.parse(dateStr, RUSSIAN_FORMATTER);
        } catch (DateTimeParseException e) {
            System.out.println("Ошибка: неверный формат даты");
            return null;
        }
    }
    
    // Метод для форматирования с учётом локали
    public static String formatDate(LocalDate date, Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter
            .ofLocalizedDate(FormatStyle.FULL)
            .withLocale(locale);
        return date.format(formatter);
    }
}

Вопросы для самопроверки

  1. Вопрос: Какой