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-ddnow() читает текущую дату в часовом поясе по умолчанию 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 366Month и 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 / +1LocalDate реализует 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.
Что стоит вынести из этого примера:
LocalDate.of(2025, Month.NOVEMBER, 4)— безопасная форма. Целочисленная перегрузка (2025, 11, 4) тоже работает, но enumMonthисключает возможность передать 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, меньший класс, те же гарантии неизменяемости.