Java TemporalAdjusters
Вычисление дат относительно других в Java с помощью TemporalAdjusters — firstDayOfMonth, next, previous.
plusDays(7) добавляет семь дней. withDayOfMonth(15) переводит на 15-е число. Эти два метода покрывают простые случаи. TemporalAdjuster — это объект в форме функции, который передаётся в Temporal.with(adjuster) для обработки случаев, когда новая дата зависит от текущей более сложным образом: «первый понедельник следующего месяца», «последний день этого квартала», «американский День благодарения следующего года».
Интерфейс содержит один метод:
@FunctionalInterface
interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}Вам обычно не нужно реализовывать его самостоятельно. Класс java.time.temporal.TemporalAdjusters (обратите внимание на s — Adjusters, множественное число) — это набор около дюжины встроенных корректоров, покрывающих типичные случаи, а LocalDate.with(adjuster) — способ их применения.
Встроенный каталог
Импортируйте статически — код читается лучше:
import static java.time.temporal.TemporalAdjusters.*;Затем:
| Корректор | Эффект |
|---|---|
firstDayOfMonth() | день → первый день того же месяца |
lastDayOfMonth() | день → последний день того же месяца |
firstDayOfNextMonth() | день → первый день следующего месяца |
firstDayOfYear() | день → 1 января того же года |
lastDayOfYear() | день → 31 декабря того же года |
firstDayOfNextYear() | день → 1 января следующего года |
next(DayOfWeek dow) | следующий dow строго после даты |
nextOrSame(DayOfWeek dow) | сегодня, если это dow, иначе следующий dow |
previous(DayOfWeek dow) | ближайший dow строго до даты |
previousOrSame(DayOfWeek dow) | сегодня, если это dow, иначе предыдущий dow |
dayOfWeekInMonth(int ord, DayOfWeek dow) | n-й день недели в месяце: dayOfWeekInMonth(3, MONDAY) → 3-й понедельник |
firstInMonth(DayOfWeek dow) | первый dow в месяце (эквивалентно dayOfWeekInMonth(1, dow)) |
lastInMonth(DayOfWeek dow) | последний dow в месяце |
Две половины отвечают на вопросы «что такое первый/последний X» (верхняя половина) и «что такое следующий/предыдущий X» (нижняя половина). Объединяйте их в цепочку, когда вопрос сложнее:
LocalDate firstMondayNextMonth = today.with(firstDayOfNextMonth()).with(nextOrSame(DayOfWeek.MONDAY));Это «первый понедельник, начиная с первого дня следующего месяца». Читайте слева направо; каждый with — это шаг.
Применение с with(adjuster)
LocalDate today = LocalDate.now();
LocalDate eom = today.with(lastDayOfMonth());
LocalDate nextMonday = today.with(next(DayOfWeek.MONDAY));
LocalDate thirdFriday = today.with(dayOfWeekInMonth(3, DayOfWeek.FRIDAY));with присутствует на каждом Temporal: LocalDate, LocalDateTime, ZonedDateTime и даже OffsetDateTime. Корректор затрагивает только компоненты даты — применение lastDayOfMonth() к LocalDateTime не изменяет время.
Корректоры являются тотальными: они всегда возвращают результат. Нет исключения «что если сегодня не в нужном месяце» — lastDayOfMonth() от 31 января всё равно вернёт 31 января (последний день — сам день).
Lambda-корректоры
Поскольку TemporalAdjuster является @FunctionalInterface, вы можете написать собственный с помощью лямбды:
TemporalAdjuster nextWorkingDay = t -> {
LocalDate d = LocalDate.from(t).plusDays(1);
while (d.getDayOfWeek() == DayOfWeek.SATURDAY || d.getDayOfWeek() == DayOfWeek.SUNDAY) {
d = d.plusDays(1);
}
return d;
};
LocalDate friday = LocalDate.of(2025, 11, 7);
LocalDate monday = friday.with(nextWorkingDay); // 2025-11-10Паттерн: преобразуйте входящий Temporal в нужный тип даты (LocalDate.from(t)), вычислите новую дату, верните её. Тип возвращаемого значения — Temporal (интерфейс), но JDK принимает конкретное возвращаемое значение.
Для разового использования это избыточно — просто встройте логику рядом с местом вызова. Для вычисления, которое вы будете использовать в нескольких местах (следующий рабочий день, последний день квартала, официальный праздник), оформление его в виде корректора сохраняет читаемость мест вызова.
ofDateAdjuster для простого случая с лямбдой
Если ваш корректор работает только с LocalDate (распространённый случай), TemporalAdjusters.ofDateAdjuster(UnaryOperator<LocalDate>) является более чистой фабрикой:
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(d -> {
LocalDate result = d.plusDays(1);
while (result.getDayOfWeek().getValue() > 5) {
result = result.plusDays(1);
}
return result;
});Лямбда принимает и возвращает LocalDate. Фабрика оборачивает её как TemporalAdjuster. Именно к этому вы прибегаете в 90% случаев при написании пользовательских корректоров.
Практический пример: бизнес-даты, праздники, конец периода
Приведённая ниже программа использует корректоры для бухгалтерского календаря: конец месяца для выставления счетов, последний рабочий день месяца для закрытия денежных позиций, первый понедельник следующего месяца для регулярного совещания по планированию, корректор «следующий рабочий день» с учётом праздников и вычисление конца квартала.
Что стоит взять из запуска:
- Встроенные корректоры покрыли граничные случаи (
firstDayOfMonth,lastDayOfMonth,firstDayOfYearи т.д.) без каких-либо арифметических действий с вашей стороны. Для «прикрепления даты к началу/концу месяца или года» они — правильный инструмент: яснее, чем ручные вычисления в стилеwithDayOfMonth(1), и защищены от неожиданностей с длиной месяца. next(MONDAY)иprevious(FRIDAY)вернули строго другие даты;nextOrSame(TUESDAY)для вторника вернул сегодня. Запомните различие строгого и «или равного» — это источник большинства ошибок «на одну неделю», когда дата начала совпадает с целевым днём недели.- Цепочка
firstDayOfNextMonth().nextOrSame(MONDAY)выразила «первый понедельник, начиная с первого дня следующего месяца» двумя шагами. Цепочка — одна строка; ручной эквивалент — шесть. Объединение корректоров в цепочку — идиоматичный способ их компоновки. NEXT_BUSINESS_DAYпропустил выходные и праздник за один шаг. НаборHOLIDAYSбыл синтетическим примером из двух элементов; реальная реализация загружала бы данные из сервиса. Форма корректора одна и та же — оберните цикл вTemporalAdjusters.ofDateAdjuster(...)и можно использовать его в вызовахtoday.with(NEXT_BUSINESS_DAY)везде.END_OF_QUARTERбыл однострочным пользовательским корректором, который определял третий месяц квартала даты и применялlastDayOfMonth(). Суть: сложные операции с датами принадлежат именованным константамTemporalAdjuster, где место вызова читается какsomeDate.with(END_OF_QUARTER)вместо встроенного шестистрочного арифметического блока. Держите логику работы с календарём в одном месте.
Что дальше
TemporalAdjusters завершают современный API java.time. Последние две главы этой части посвящены устаревшим типам, которые вы встретите в старом коде: Java Legacy Date Class для оригинального java.util.Date и Java Calendar Class для java.util.Calendar. Оба по-прежнему существуют для обеспечения совместимости; для обоих существует чистый путь преобразования в java.time.