W3docs

Java LocalDate

Работа с датами без времени и часового пояса в Java: создание, изменение и запросы LocalDate.

LocalDate — это календарная дата: год, месяц, день — без времени суток и без часового пояса. Это одна и та же дата на любых часах в любой точке мира: когда вы пишете LocalDate.of(2025, 11, 4), это четвёртое ноября в ISO-календаре, и ничего больше. Никакого 14:30, никакого UTC-смещения, никаких разночтений между Токио и Гонолулу.

Это делает его правильным типом для многих задач, с которыми устаревший java.util.Date справлялся плохо: дни рождения, даты контрактов, даты счётов, дата, выбранная в UI-датапикере. Везде, где единица измерения — один календарный день, нужно использовать LocalDate.

Создание

Три стандартных фабричных метода:

LocalDate today  = LocalDate.now();                          // system default zone
LocalDate stardate = LocalDate.of(2025, 11, 4);              // year, month (1-12), day (1-31)
LocalDate parsed = LocalDate.parse("2025-11-04");            // ISO-8601 yyyy-MM-dd

now() читает текущую дату в часовом поясе по умолчанию JVM. Это почти всегда то, что вам нужно; в тестах это проблема, и формы с перегрузкой Clock (LocalDate.now(clock)) позволяют подставить фиксированные часы. Глава о разборе описывает parse с пользовательскими форматами; по умолчанию принимаются только даты ISO-8601.

Также можно использовать enum Month вместо целого числа 1..12:

LocalDate.of(2025, Month.NOVEMBER, 4);                       // type-safe; no risk of using 0 for January

Если вы когда-либо писали new GregorianCalendar(2025, 11, 4) и получали декабрь (потому что устаревший API использует месяцы с нуля), форма с enum — это именно то, что вам нужно.

Получение полей

Каталог методов-аксессоров:

int year      = date.getYear();
Month month   = date.getMonth();                              // enum
int monthVal  = date.getMonthValue();                         // 1-12
int day       = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();                          // enum: MONDAY, TUESDAY, ...
int dayOfYear = date.getDayOfYear();                          // 1-366
boolean leap  = date.isLeapYear();
int monthLen  = date.lengthOfMonth();                         // 28-31
int yearLen   = date.lengthOfYear();                          // 365 or 366

Month и DayOfWeek — это enum-ы. Используйте их; они делают код, сравнивающий конкретный день или месяц, значительно понятнее:

if (date.getDayOfWeek() == DayOfWeek.MONDAY) ...              // type-safe
if (date.getMonth() == Month.NOVEMBER) ...                    // no off-by-one risk

У каждого enum есть собственные вспомогательные методы: Month.length(boolean leap), DayOfWeek.getValue() возвращает 1-7, где понедельник = 1, и DayOfWeek.plus(7) для «тот же день через n дней».

Изменение — каждый метод возвращает новый экземпляр

Арифметические методы:

date.plusDays(7);                                              // a week later
date.plusWeeks(2);
date.plusMonths(1);                                            // careful: month length varies
date.plusYears(1);

date.minusDays(30);
date.minusYears(5);

И формы «заменить одно поле»:

date.withYear(2026);
date.withMonth(1);
date.withDayOfMonth(1);
date.withDayOfYear(1);                                         // first day of the year

Каждый из этих методов возвращает новый LocalDate. Оригинал не изменяется. Написать date.plusDays(7) и забыть сохранить результат — это холостая операция и ошибка, которую хотя бы раз допускал каждый.

Оговорка о «переменной длине месяца» для plusMonths: когда добавление месяца приводит к дате, которой не существует в целевом месяце, java.time усекает до последнего дня. LocalDate.of(2025, 1, 31).plusMonths(1) равно 2025-02-28 (или 02-29 в високосный год), а не 2025-03-03. Поведение задокументировано и последовательно, но это означает, что plusMonths(1) и minusMonths(1) не всегда являются обратными операциями.

Сравнение

date.isBefore(other);
date.isAfter(other);
date.isEqual(other);                                           // same as equals here; useful on ZonedDateTime
date.compareTo(other);                                         // -1 / 0 / +1

LocalDate реализует Comparable<LocalDate>, поэтому естественно сортируется в любой коллекции. Для проверки «находится ли эта дата в диапазоне [start, end]?» типичная конструкция: !date.isBefore(start) && !date.isAfter(end).

Разница: until и ChronoUnit.between

Сколько дней между двумя датами?

long days = ChronoUnit.DAYS.between(start, end);               // a long; signed
long weeks = ChronoUnit.WEEKS.between(start, end);
long months = ChronoUnit.MONTHS.between(start, end);

Period diff = start.until(end);                                // a Period (years/months/days)

ChronoUnit.X.between — правильный вызов для вопроса «сколько целых X между этими датами?». until возвращает Period — разбивку в виде календарных единиц, полезную для «вы являетесь участником уже 2 года, 3 месяца и 14 дней».

Обратите внимание на соглашение о знаке: between(start, end) положительно, когда end после start, и отрицательно в противном случае.

Краткий способ узнать «какой день недели это...»

Пакет temporal-adjusters даёт вам предикаты, которые иначе пришлось бы вычислять вручную:

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

date.with(firstDayOfMonth());
date.with(lastDayOfMonth());
date.with(firstDayOfNextMonth());
date.with(next(DayOfWeek.MONDAY));                             // next Monday strictly after `date`
date.with(nextOrSame(DayOfWeek.MONDAY));                       // today if today is Monday, else next
date.with(previousOrSame(DayOfWeek.SUNDAY));
date.with(lastInMonth(DayOfWeek.FRIDAY));                      // last Friday of the month

Глава Temporal Adjusters рассматривает их подробнее. Пока главный вывод: не пишите «следующий понедельник после этой даты» вручную; у adjusters это уже есть.

Оговорка о часовом поясе

LocalDate не содержит часового пояса, поэтому LocalDate.now() должен выбрать один, чтобы понять, какой календарный день сейчас «сегодня». По умолчанию используется часовой пояс JVM (ZoneId.systemDefault()). Если сервер настроен на UTC, а местное время в Нью-Йорке 23:30, LocalDate.now() вернёт завтрашнюю дату с нью-йоркской точки зрения — потому что зона JVM говорит, что уже за полночь UTC.

Для даты, локальной для известного часового пояса, передайте его явно:

LocalDate tokyoToday = LocalDate.now(ZoneId.of("Asia/Tokyo"));

Это аукается в продакшене именно тогда, когда ноутбук разработчика находится в другом часовом поясе, чем развёрнутый сервер. Указывайте часовой пояс явно, когда он важен.

Практический пример: арифметика дат счёта

Программа ниже использует LocalDate для задач, которые решала бы небольшая система выставления счётов: сгенерировать дату счёта, вычислить сроки оплаты, подсчитать дни просрочки, найти конец месяца и следующий рабочий день. Это реальная форма кода с LocalDate.

java— editable, runs on the server

Что стоит вынести из этого примера:

  • LocalDate.of(2025, Month.NOVEMBER, 4) — безопасная форма. Целочисленная перегрузка (2025, 11, 4) тоже работает, но enum Month исключает возможность передать 0 для января — ошибку, унаследованную от GregorianCalendar. Когда второй аргумент может быть любым, используйте enum.
  • plusDays(30) вернул новый LocalDate; вывод оригинала в конце программы показал, что он не изменился. Каждый арифметический метод и with*-метод следует этому правилу, что делает тип потокобезопасным по конструкции. Никакого защитного копирования не требуется; передача LocalDate в метод всегда безопасна.
  • Демонстрация plusMonths(1) показала поведение усечения: 31 января + 1 месяц = 28 февраля (или 29 в високосный год). Поведение задокументировано и последовательно, но jan31.plusMonths(1).minusMonths(1) вернёт 28 января, а не 31 января. Круговое преобразование с получением оригинала работает для plusDays/minusDays, но не для plusMonths/minusMonths.
  • Temporal adjusters (lastDayOfMonth, firstDayOfNextMonth, nextOrSame(MONDAY)) заменили многострочные ручные обходы календаря. В цепочке они выражают «первый понедельник, начиная с первого числа следующего месяца» двумя вызовами. Следующая глава о LocalTime и отдельная глава о Temporal Adjusters рассматривают их подробнее.
  • ChronoUnit.DAYS.between(invoice, today) вернул знаковое значение long. Метод invoiceDate.until(today) вернул Period — в виде календарных единиц с отдельными полями год/месяц/день. Они отвечают на разные вопросы: ChronoUnit.X для «сколько целых X», Period для «в удобном для человека формате». Выбирайте тот, чья форма соответствует нужному выводу.

Что дальше

LocalDate — это сторона даты. Следующая глава, Java LocalTime, — её зеркало: время суток без даты и без часового пояса. Тот же текучий API, меньший класс, те же гарантии неизменяемости.

Практика

Практика
`LocalDate.of(2025, 1, 31).plusMonths(1)` — какое значение вернёт этот вызов и почему?
`LocalDate.of(2025, 1, 31).plusMonths(1)` — какое значение вернёт этот вызов и почему?
Was this page helpful?