W3docs

Java LocalDateTime

Объединяйте дату и время без информации о часовом поясе в Java с помощью LocalDateTime.

LocalDateTime — это LocalDate и LocalTime, склеенные вместе: календарная дата и время суток, при этом по-прежнему без часового пояса. Это естественный тип для значений вида «это событие произошло в 14:30 4 ноября», и он является наиболее используемым типом java.time в реальных проектах, которые не имеют дела с глобальными пользователями.

Fluent API по форме идентичен обеим половинам — те же фабричные методы now/of/parse, те же модификаторы plusX/minusX/withX, те же сравнения isBefore/isAfter. Интересны именно новые возможности: объединение с датой или временем, обратное разложение и явное преобразование в ZonedDateTime, когда часовой пояс наконец становится важен.

Создание

LocalDateTime now    = LocalDateTime.now();                  // JVM default zone
LocalDateTime made   = LocalDateTime.of(2025, 11, 4, 14, 30);
LocalDateTime made2  = LocalDateTime.of(2025, Month.NOVEMBER, 4, 14, 30, 15);
LocalDateTime made3  = LocalDateTime.of(LocalDate.of(2025, 11, 4), LocalTime.of(14, 30));
LocalDateTime parsed = LocalDateTime.parse("2025-11-04T14:30:00");   // ISO-8601

Форма ISO-8601 содержит литеральный символ T между датой и временем — это стандартный разделитель. LocalDateTime.parse("2025-11-04 14:30:00") (пробел вместо T) не будет разобран с настройками по умолчанию; для этого потребуется пользовательский DateTimeFormatter.

Разложение

LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

Эти методы являются обратными к LocalDate.atTime(time) / LocalTime.atDate(date). Оба — чистая проекция: без потери информации, без введения часового пояса.

Все аксессоры сразу

LocalDateTime наследует меню аксессоров от обеих половин:

dt.getYear();          dt.getMonth();        dt.getMonthValue();
dt.getDayOfMonth();    dt.getDayOfWeek();    dt.getDayOfYear();
dt.getHour();          dt.getMinute();       dt.getSecond();          dt.getNano();

Те же имена, та же семантика. Вам не нужно вызывать toLocalDate() или toLocalTime(), чтобы получить доступ к отдельным компонентам — они все доступны напрямую.

Арифметика через границу

Ключевое отличие от LocalTime: LocalDateTime.plusHours(3) на значении 23:00 не переполняется молча. Оно переходит на следующий день:

LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3);   // 2025-11-05T02:00 — date advanced as expected

Именно поэтому следует использовать LocalDateTime вместо LocalTime для любых вычислений, которые могут пересекать полночь. Математика согласуется с тем, что вы ожидаете от настоящих часов, которые знают, какой сегодня день.

dt.plusDays(7);          dt.plusHours(36);        dt.plusMinutes(150);
dt.minusYears(1);        dt.minusSeconds(45);

dt.withYear(2026);       dt.withHour(0);          dt.withMinute(0);

Правило усечения plusMonths из LocalDate применяется и здесь: LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) даёт 2025-02-28T12:00, а не 2025-03-03T12:00. Усечение применяется только к компоненту даты; время остаётся неизменным.

Часовой пояс намеренно отсутствует

LocalDateTime — это не момент на глобальной временной шкале. LocalDateTime.of(2025, 11, 4, 9, 0) может означать 9 утра в Нью-Йорке, 9 утра в Берлине или 9 утра в Токио — три совершенно разных Instant, и LocalDateTime не скажет вам, какой именно. Если два LocalDateTime равны, это означает равенство строк даты и времени; это не означает равенство лежащих в основе моментов.

Это особенность, а не ошибка. Для «контракт подписывается в 14:00 по местному времени там, где находится подписант» LocalDateTime — именно правильный тип. Для «сервер получил запрос в...» — неправильный тип: используйте Instant. Для «встреча начинается в 14:00 по нью-йоркскому времени» — тоже неправильный: используйте ZonedDateTime.

Чтобы преобразовать значение в момент с часовым поясом, необходимо явно добавить пояс:

ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant       inst = ldt.atZone(ZoneId.systemDefault()).toInstant();

Вызов atZone(...) является ключевым — именно в этот момент система типов заставляет вас решить, какой часовой пояс вы имеете в виду. После принятия решения преобразование в Instant становится механическим. Следующие две главы (ZonedDateTime, Instant) подробно рассматривают типы с часовым поясом и глобальные типы.

Сравнение

dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);

Порядок лексикографический по (date, time). То же предупреждение, что и прежде: два LocalDateTime сравниваются по строковым представлениям даты и времени, а не по лежащим в основе моментам — потому что без часового пояса никаких «лежащих в основе моментов» не существует.

Расстояние

ChronoUnit.X.between работает напрямую:

long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);

Duration.between работает с LocalDateTime (как и с любым Temporal). Для чисто календарной арифметики — «сколько месяцев между двумя LocalDateTime» — используйте ChronoUnit.MONTHS.between, который возвращает long, или Period.between(start.toLocalDate(), end.toLocalDate()) для разбивки в календарном виде.

Практический пример: планирование через полночь

Программа ниже использует LocalDateTime для небольшого кода планирования: ночная смена начинается в 22:00 и заканчивается в 06:00, продолжительность вычисляется корректно через полночь; «сейчас» округляется вверх до следующей четверти часа; находится следующее вхождение повторяющейся встречи в 09:30; а также демонстрируется правило «зону необходимо добавить явно» при преобразовании в момент времени.

java— editable, runs on the server

Что важно вынести из запуска:

  • Duration.between(startShift, endShift) вернул PT8H. Смена пересекла полночь, и компоненты даты выполнили перенос — никакой неоднозначности. То же вычисление на чистых LocalTime вернуло бы PT-16H (ловушка LocalTime из предыдущей главы). Для арифметики, которая может пересекать полночь, LocalDateTime — правильный тип.
  • Начиная с 20:00, plusHours(3) осталось на 11-04 (23:00, полночь не пересечена); plusHours(5) перешло вперёд на 11-05T01:00. Семейство plus/minus у LocalDateTime корректно распространяет переносы по всей цепочке Y/M/D/h/m/s/ns. Никакой специальной обработки в вашем коде не требуется.
  • Блок «следующая встреча в 09:30» построил сегодняшнее значение 09:30 тремя вызовами withX, затем выбрал между «сегодня» и «завтра» на основе isBefore. Это стандартный шаблон для «следующего повторяющегося события в данное время суток» — достаточно компактный, чтобы встроить его inline, и достаточно распространённый, чтобы вынести в вспомогательный метод, если таких случаев много.
  • Блок «одинаковый LocalDateTime, разные зоны» дал два разных Instant с разницей в шесть часов. Это главная причина, по которой LocalDateTime не претендует на то, чтобы быть моментом. Класс отказывается делать вид, что даты и времени вместе достаточно; это надпись на настенных часах где-то, а на каких именно — зависит от предоставленного вами часового пояса.
  • Итоговая проверка иммутабельности показала, что now не изменился после plusDays(7).withHour(0).withMinute(0). Эта гарантия сохраняется для каждой операции, каждой цепочки, каждого вспомогательного метода — не существует способа изменить LocalDateTime. Передавайте его свободно, используйте совместно в потоках, храните в Map.

Что дальше

LocalDateTime — последний из трёх «Local»-типов: без часового пояса, без претензий быть моментом. Следующая глава, Java ZonedDateTime, явно добавляет часовой пояс: LocalDateTime плюс ZoneId плюс разрешённое смещение для данного местного времени в данном поясе — вместе они фиксируют реальный момент на глобальной временной шкале.

Практика

Практика
Два разработчика — один в Нью-Йорке, другой в Берлине — оба создают значение `LocalDateTime.of(2025, 11, 4, 14, 0)`. Это один и тот же момент времени?
Два разработчика — один в Нью-Йорке, другой в Берлине — оба создают значение `LocalDateTime.of(2025, 11, 4, 14, 0)`. Это один и тот же момент времени?
Was this page helpful?