W3docs

Java Period

Представляйте календарные промежутки (годы, месяцы, дни) в Java с помощью Period.

Period — это «календарный» брат Duration. Если Duration — это «X секунд плюс Y наносекунд», то Period — это «X лет, Y месяцев, Z дней». Это правильный тип для любого промежутка, который вы выразили бы в календарных единицах: «пробный период 30 дней», «годовая подписка», «двухмесячный срок уведомления», «прибавить один платёжный цикл к дате продления».

Эти два типа никогда не смешиваются. Duration.ofDays(30) — это ровно 30 × 24 × 3600 секунд. Period.ofDays(30) — это 30 календарных дней, которые обычно, но не всегда равны 30 × 24 часам (при переходе на летнее/зимнее время добавляется или убирается час). Для «точного количества секунд» используйте Duration. Для «календарного дня, который наступит через N дней», — Period.

Создание

Period.ofDays(30);
Period.ofWeeks(2);                                           // stored as 14 days
Period.ofMonths(3);
Period.ofYears(1);
Period.of(1, 6, 0);                                          // 1 year, 6 months, 0 days

Period.between(startDate, endDate);                           // takes LocalDate (not LocalDateTime)
Period.parse("P1Y2M3D");                                     // ISO-8601: P[years]Y[months]M[days]D

Строковый формат — PnYnMnD: P1Y2M3D — один год, два месяца, три дня. Префикс P обязателен. Буква T не используется (это сделало бы строку Duration); часы, минуты и секунды не поддерживаются (они здесь неуместны).

Period.between(start, end) принимает два значения LocalDate и возвращает разбивку разницы:

Period age = Period.between(LocalDate.of(1990, 3, 15), LocalDate.of(2025, 11, 4));
// P35Y7M20D — 35 years, 7 months, 20 days

Это стандартный способ «вычислить возраст». Результат — разбивка, а не одно число: в нём 35 лет, затем 7 месяцев сверху, затем 20 дней. Чтобы свести к одному значению, используйте ChronoUnit.YEARS.between(...), возвращающий long.

Инспекция

Period p = Period.of(1, 6, 14);
p.getYears();      // 1
p.getMonths();     // 6
p.getDays();       // 14

p.toTotalMonths();                                            // 1 * 12 + 6 = 18 (years + months, ignoring days)
p.isZero();                                                   // false
p.isNegative();                                               // true if any component is negative

Три метода доступа для трёх компонентов, плюс toTotalMonths для быстрой агрегации. Метода toTotalDays нет — для этого потребовался бы календарный контекст (год содержит 365 или 366 дней; месяц — от 28 до 31).

Арифметика

p.plus(Period.ofMonths(1));
p.plusYears(1);
p.plusMonths(6);
p.plusDays(14);
p.minus(Period.ofDays(7));

p.multipliedBy(3);
p.negated();
p.normalized();                                               // collapse extra months into years

normalized() — интересный метод: он преобразует любое количество месяцев от 12 и более в годы. Period.of(0, 14, 0).normalized() даёт Period.of(1, 2, 0). Дни он не затрагивает — «нормализовать 31 день в 1 месяц и 1 день» невозможно, потому что месяцы имеют разную длину.

Прибавление к дате

Period реализует TemporalAmount. Любой «датоподобный» Temporal принимает его:

LocalDate maturity = LocalDate.of(2025, 11, 4).plus(Period.ofMonths(6));
LocalDate retirement = LocalDate.of(1990, 3, 15).plus(Period.ofYears(65));

LocalDateTime renewal = LocalDateTime.of(2025, 11, 4, 9, 0).plus(Period.ofYears(1));
ZonedDateTime nextBill = zdt.plus(Period.ofMonths(1));

Прибавление месячной или годовой части Period к Instant выбрасывает UnsupportedTemporalTypeExceptionInstant является точкой на временной шкале без календаря, поэтому JDK отказывается вычислять «момент через один месяц» без часового пояса. (Дневная часть допустима: Instant.plus(Period.ofDays(1)) работает, потому что JDK считает день ровно 86 400 секундами. Только месяцы и годы не имеют фиксированной длины и потому не имеют смысла для Instant.) Когда нужна календарная арифметика, выполните конвертацию через ZonedDateTime:

Instant nextMonth = inst.atZone(ZoneId.of("UTC"))
                        .plus(Period.ofMonths(1))
                        .toInstant();

Эта намеренная многословность — место, где вы предоставляете недостающий календарный контекст.

Правило усечения plusMonths из главы LocalDate применяется к арифметике с Period точно так же: 31 января + Period.ofMonths(1) даёт 28 февраля, а не 3 марта.

Period не нормализует компоненты между собой

Тонкое поведение: Period.of(1, 0, 365) не равен Period.of(2, 0, 0), даже если при прибавлении к обычной дате они описывают одинаковый промежуток. Класс хранит разбивку дословно и сравнивает по структуре:

Period.of(1, 0, 365).equals(Period.of(2, 0, 0));              // false
Period.of(0, 14, 0).equals(Period.of(1, 2, 0));               // false (until normalized())
Period.of(0, 14, 0).normalized().equals(Period.of(1, 2, 0));  // true

Чтобы проверить «является ли этот период хотя бы годом независимо от разбивки», сравнивайте по датам: start.plus(p1).isEqual(start.plus(p2)) — единственная полностью корректная проверка.

Разница: Period.between против ChronoUnit.between

Period diff = Period.between(start, end);                     // calendar breakdown
long days   = ChronoUnit.DAYS.between(start, end);            // single long
long months = ChronoUnit.MONTHS.between(start, end);
long years  = ChronoUnit.YEARS.between(start, end);

Оба отвечают на разные вопросы:

  • Period.between(start, end) возвращает «1 год, 6 месяцев, 14 дней» — полезно, когда нужно отобразить разбивку.
  • ChronoUnit.DAYS.between(start, end) возвращает 567 (или столько дней, сколько есть в действительности) — полезно для сравнения или накопления.

Используйте второй вариант, когда нужно выполнять арифметические операции с результатом. Используйте первый, когда хотите показать пользователю.

Практический пример: подписки, пробные периоды и возраст

Программа ниже использует Period для небольшого сценария подписки: пробный период заканчивается через месяц после регистрации, дата продления повторяется ежегодно, возраст клиента вычисляется по дате рождения, а поведение усечения на границах месяцев показано явно. Также демонстрируется контраст с Duration для «того же промежутка в реальном времени».

java— editable, runs on the server

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

  • Прибавление Period.ofMonths(1) к 31 января дало 28 февраля — то же правило усечения, что и в LocalDate. Period.plusMonths(1).minusMonths(1) не всегда является тождественным преобразованием. Если вы вычисляете даты выставления счёта вблизи конца месяца, учитывайте усечение явно (например, всегда выставляйте счёт 1-го числа следующего месяца), а не рассчитывайте на симметрию туда-обратно.
  • Period.between(birth, today) вернул календарную разбивку — годы, месяцы, дни. Чтобы проверить «является ли человек совершеннолетним», используйте ChronoUnit.YEARS.between(birth, today) >= 18, а не age.getYears() >= 18. В данном случае оба дают одинаковый ответ, но в общем они отвечают на разные вопросы — ChronoUnit.YEARS.between возвращает общее количество полных лет, age.getYears() — компонент лет в разбивке.
  • Period.of(0, 14, 0).normalized() стал Period.of(1, 2, 0). Количество дней не изменилось — дни нельзя нормализовать, не зная, какие месяцы задействованы. Если вы формируете Period арифметически и хотите получить «чистое» представление, вызовите normalized перед сохранением или отображением.
  • P1Y.equals(P12M) вернул false, и P1Y.equals(P365D) тоже false. Равенство структурное, а не по длине. Применённые к 2024-01-31 (високосному году), + P1Y и + P12M оба дали 2025-01-31, а + P365D дал 2025-01-30 — на день меньше, потому что в 2024 году 366 дней. Так что даже «одинаковая длина» зависит от даты применения. Если вам действительно нужно «дают ли эти два Period одинаковую конечную дату?», вычислите обе конечные даты и сравните с LocalDate.isEqual. Форма .normalized() решает проблему год/месяц, но никогда — проблему дней.
  • Вызов inst.plus(Period.ofMonths(1)) выбросил UnsupportedTemporalTypeException. Instant не имеет календаря, поэтому месяц (чья длина меняется) не имеет смысла для него. Дневная часть Period допустима для Instant — день — это ровно 86 400 секунд — но месяцы и годы нет. Сначала выполните конвертацию через ZonedDateTime; система типов заставляет явно выбрать часовой пояс. Зеркальная ошибка из главы о Duration (Duration для LocalDate) — та же концепция: JDK отказывается придумывать недостающий контекст.

Что дальше

Period завершает пару «промежутки времени». Следующие две главы посвящены границе строка ↔ значение: Форматирование дат в Java — преобразование значений java.time в строки, и Разбор дат в Java — обратная операция. Обе используют DateTimeFormatter — современную потокобезопасную замену устаревшего SimpleDateFormat.

Практика

Практика
Пользователь регистрируется 31 января 2025 года. Ваш код выставления счёта вычисляет следующее списание с помощью `signupDate.plus(Period.ofMonths(1))`. Какая будет дата следующего списания и что нужно знать об этом поведении?
Пользователь регистрируется 31 января 2025 года. Ваш код выставления счёта вычисляет следующее списание с помощью `signupDate.plus(Period.ofMonths(1))`. Какая будет дата следующего списания и что нужно знать об этом поведении?
Was this page helpful?