W3docs

Разбор дат в Java

Преобразование строк в объекты даты-времени Java с помощью DateTimeFormatter и обработка исключений при разборе.

Разбор — это зеркало форматирования. Тот же DateTimeFormatter, тот же алфавит шаблонов, те же оговорки — но строка читается, а не записывается. Каждый тип java.time имеет фабричный метод parse(...); с шаблоном по умолчанию (ISO-8601) он принимает один аргумент, а с пользовательским шаблоном — форматтер.

LocalDate    d  = LocalDate.parse("2025-11-04");                                       // ISO default
LocalTime    t  = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.parse("2025-11-04T14:30:00");                          // note the T
ZonedDateTime zdt = ZonedDateTime.parse("2025-11-04T14:30:00-05:00[America/New_York]");
Instant       i = Instant.parse("2025-11-04T19:30:00Z");                                // trailing Z mandatory

Каждый из этих методов использует форматтер типа по умолчанию — строгий ISO-8601. Для всего, что не соответствует формату ISO, нужно создать форматтер и передать его.

Разбор с пользовательским шаблоном

DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate d = LocalDate.parse("04/11/2025", f);                                         // British DMY

Алфавит шаблонов такой же, как для форматирования — dd MMM yyyy, HH:mm:ss, MM/dd/yyyy h:mm a. Правило сопоставления строгое по умолчанию: каждый литеральный символ в шаблоне должен присутствовать во входной строке точь-в-точь, и каждое поле должно содержать правильное количество цифр.

DateTimeFormatter dmy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate.parse("4/11/2025", dmy);                                                       // FAILS: "dd" requires 2 digits
LocalDate.parse("04/11/2025", dmy);                                                      // OK

Если входная строка иногда содержит однозначные числа, используйте d/M/yyyy (который принимает 1-2 цифры) или создайте форматтер с DateTimeFormatterBuilder и parseStrict(false). Однобуквенная форма — более простое решение.

Локаль важна при разборе

Та же история с локалью, что и при форматировании: названия месяцев (MMMM) и дней недели (EEEE) зависят от языка, поэтому форматтер должен знать, на каком языке написана входная строка.

DateTimeFormatter englishDay = DateTimeFormatter.ofPattern("EEEE, MMMM d yyyy", Locale.US);
DateTimeFormatter germanDay  = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy",  Locale.GERMAN);

LocalDate.parse("Tuesday, November 4 2025", englishDay);   // 2025-11-04
LocalDate.parse("Dienstag, 4. November 2025", germanDay);  // 2025-11-04 (German month name)

Обратите внимание на yyyy в обоих шаблонах. Для получения LocalDate входная строка должна содержать год — шаблон вроде "EEEE, MMMM d" парсится, но только в TemporalAccessor без поля года, поэтому LocalDate.parse на нём выбрасывает исключение. Если ваши строки действительно не содержат года, разберите их в TemporalAccessor и объедините с годом самостоятельно.

Без явной локали используется Locale.getDefault() — а локаль по умолчанию серверной JVM непредсказуема. Всегда передавайте локаль при разборе названий месяцев или дней недели из строки, которую мог ввести пользователь. Зеркало принципа «всегда форматировать с локалью» применяется и здесь.

DateTimeParseException

При неудачном разборе выбрасывается DateTimeParseException (подкласс RuntimeException, поэтому не объявляется в сигнатуре parse). Сообщение содержит как позицию, так и то, что ожидалось:

try {
  LocalDate.parse("2025-13-45");                              // month 13, day 45
} catch (DateTimeParseException e) {
  e.getParsedString();                                         // "2025-13-45"
  e.getErrorIndex();                                           // index where parsing gave up
  e.getMessage();                                              // human description
}

Здесь возникают два различных вида ошибок:

  • Несоответствие формата. Строка вообще не подходит под шаблон — "04 nov 2025" против "dd-MM-yyyy".
  • Значение вне допустимого диапазона. Строка соответствует шаблону, но значение невозможно — месяц 13, день 32.

Оба случая выбрасывают один и тот же класс. Перехватывайте и сообщайте об ошибке; никогда не замалчивайте её.

Двузначные годы и ловушка MAX_VALUE

Шаблон yy (двузначный год) имеет задокументированное, но неожиданное поведение по умолчанию: он разбирается в год, ближайший к сегодняшнему в рамках 100-летнего окна. LocalDate.parse("11/04/25", DateTimeFormatter.ofPattern("MM/dd/yy")) даёт 2025-11-04 в 2025 году и 2125-11-04 в 2076 году. Это удобная функция для случаев «близко к сегодня» и подводный камень для архивных данных.

Решение — использовать yyyy, когда входная строка содержит четыре цифры, и явно указывать столетнее окно, когда это не так:

DateTimeFormatter f = new DateTimeFormatterBuilder()
    .appendPattern("MM/dd/")
    .appendValueReduced(ChronoField.YEAR, 2, 2, 1950)         // window starts at 1950
    .toFormatter();

Если вы работаете с устаревшими данными с yy, задокументируйте окно в коде. По умолчанию используется «скользящая опорная точка вокруг текущего года» — а это не то, что нужно, если «все мои данные из 1980-х».

Разбор без привязки к конкретному типу

DateTimeFormatter.parse(String) возвращает TemporalAccessor — нижний уровень иерархии типов. Полезно, когда входная строка может быть как LocalDate, так и LocalDateTime:

TemporalAccessor ta = DateTimeFormatter.ISO_DATE_TIME.parseBest(
    "2025-11-04T14:30:00",
    LocalDateTime::from,                                       // preferred
    LocalDate::from);                                          // fallback

parseBest(text, ...queries) пробует метод from каждого типа по порядку и возвращает первый успешный результат. Для дальнейшего использования результат требует проверки через instanceof:

if (ta instanceof LocalDateTime ldt) ...
else if (ta instanceof LocalDate ld) ...

В большинстве случаев проще вызывать parse(...) непосредственно у конкретного типа. parseBest нужен тогда, когда вы принимаете данные нескольких форм (например, столбец CSV, который может содержать дату или дату со временем).

Нестрогий разбор с помощью строителя

DateTimeFormatterBuilder позволяет создать более гибкий форматтер:

DateTimeFormatter lenient = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd[ HH:mm[:ss]]")                  // optional sections in []
    .parseLenient()                                            // accept missing leading zeros etc.
    .parseCaseInsensitive()                                    // ignore case on month/day names
    .toFormatter(Locale.US);

Синтаксис с квадратными скобками [...] обозначает необязательную секцию — этот шаблон разбирает и "2025-11-04" (без времени), и "2025-11-04 14:30" (со временем). В сочетании с parseLenient и parseCaseInsensitive можно создать форматтер, принимающий более широкий диапазон входных данных без написания собственного парсера.

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

Instant.parse является строгим

Instant.parse("2025-11-04T19:30:00Z") работает. Завершающий Z (UTC) обязателен; любое другое смещение (-05:00, +09:00) требует сначала OffsetDateTime.parse или ZonedDateTime.parse, а затем .toInstant():

Instant inst = OffsetDateTime.parse("2025-11-04T14:30:00-05:00").toInstant();

Это канонический способ преобразования, когда внешний API передаёт строки ISO-8601 со смещением часового пояса, но без IANA-зоны.

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

Программа ниже разбирает три строки в формате дат из синтетического конфига: дату в формате ISO, дату с пользовательским шаблоном и мгновение в формате ISO. Затем демонстрируется нестрогий строитель с необязательной секцией времени, API parseBest для «либо дата, либо дата-время» и поведение при несоответствии входной строки.

java— editable, runs on the server

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

  • Три ISO-парсера по умолчанию каждый принял ровно стандартную форму: yyyy-MM-dd для LocalDate, форму с разделителем T для LocalDateTime и форму с завершающим Z для Instant. Никакой гибкости, никаких догадок — и это правильно. Если входная строка подходит, форматтер не нужен; если нет, создание форматтера занимает одну строку.
  • Нестрогий форматтер принял три разных формы входных данных — только дата, дата со временем, дата со временем и секундами — потому что секция в квадратных скобках [...] является необязательной. parseBest(text, LocalDateTime::from, LocalDate::from) выбрал наиболее полный тип, поддерживаемый каждой входной строкой. Это правильный подход, когда вы принимаете введённые пользователем или конфигурационные даты с переменной точностью.
  • OffsetDateTime.parse(wire).toInstant() стал каноническим мостом от «временной метки ISO-8601 со смещением» к Instant. Сам Instant.parse принимает только UTC со суффиксом Z; всё остальное должно проходить через OffsetDateTime (или ZonedDateTime) сначала. Преобразование занимает по одной строке в каждую сторону.
  • Разбор с немецкой локалью сработал только потому, что форматтер был создан с Locale.GERMAN. Локаль по умолчанию отвергла бы "November", если JVM работает в немецкой локали (которая ожидает, что "November" из Locale.GERMAN сопоставляется с немецкими названиями). Всегда фиксируйте локаль на этапе разбора — одного форматтера недостаточно; локаль управляет разрешением названий месяцев и дней недели.
  • Оба блока обработки ошибок выбросили DateTimeParseException с полезной информацией о позиции. getErrorIndex — это место, где парсер остановился; getParsedString — входная строка так, как её увидел парсер. Отображайте эту информацию в пользовательских сообщениях об ошибках — «не удалось разобрать дату на символе 5» несравнимо полезнее, чем «неверная дата».

Что дальше

Форматирование и разбор завершают работу со строковой границей. Следующая глава, Java Temporal Adjusters, возвращается к работе со значениями и рассматривает встроенные корректоры (firstDayOfMonth, nextOrSame(MONDAY) и т.д.), а также способы создания собственных — полезно всякий раз, когда нужная вам дата зависит от имеющейся («первый вторник после 15-го числа»).

Практика

Практика
Веб-форма позволяет пользователям вводить дату в формате '04/11/2025' (день/месяц/год). Ваш код использует `LocalDate.parse(input, DateTimeFormatter.ofPattern('dd/MM/yyyy'))` и затем проверяет 'является ли эта дата будущей?'. Пользователь вводит '4/11/2025' (без ведущего нуля для дня), и разбор выбрасывает исключение. Какое минимальное исправление нужно применить?
Веб-форма позволяет пользователям вводить дату в формате '04/11/2025' (день/месяц/год). Ваш код использует `LocalDate.parse(input, DateTimeFormatter.ofPattern('dd/MM/yyyy'))` и затем проверяет 'является ли эта дата будущей?'. Пользователь вводит '4/11/2025' (без ведущего нуля для дня), и разбор выбрасывает исключение. Какое минимальное исправление нужно применить?
Was this page helpful?