Работа с датой и временем в 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 внутри делает примерно следующее:
- Проверяет, существует ли такая дата (нет ли 30 февраля)
- Вычисляет количество дней от 1970 года до этой даты
- Сохраняет это число в памяти
В 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"));
Лучшие практики и советы профессионалов
- Всегда используйте
java.time. Никогда не используйтеjava.util.Dateилиjava.util.Calendarв новом коде. - Храните даты в UTC. В базах данных и при передаче между системами храните время в UTC. Конвертируйте в локальное время только для отображения пользователю.
- Используйте
Instantдля машинного времени. Если вам нужно просто зафиксировать момент (например, время создания записи), используйтеInstant— он быстрее и проще. - Будьте осторожны с
LocalDateTime. Он не содержит информации о часовом поясе. Используйте его только если часовой пояс не важен (например, день рождения). - Проверяйте даты на валидность. Всегда проверяйте, что пользователь ввёл корректную дату, особенно если дата вводится вручную.
- Используйте
DateTimeFormatterс локалью. Для отображения даты пользователю учитывайте его локаль (язык, страну). - Не создавайте
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);
}
}
Вопросы для самопроверки
-
Вопрос: Какой