Класс DateTimeFormatter в Java: форматирование и разбор дат - Вопросы
Всего: 6 вопросов
1. Что такое DateTimeFormatter в Java и как с его помощью превратить дату в строку и строку в дату?
Что такое DateTimeFormatter в Java и как с его помощью превратить дату в строку и строку в дату?
java.time.format.DateTimeFormatter — неизменяемый потокобезопасный класс из Java 8, который форматирует дату и время в строку и разбирает строку обратно. Его создают по шаблону ofPattern("dd.MM.uuuu") или ofPattern(pattern, Locale), берут готовую константу (ISO_LOCAL_DATE) или локализованный формат ofLocalizedDate(FormatStyle). Буквы, которые не являются символами шаблона, берут в одинарные кавычки: "yyyy-MM-dd'T'HH:mm".
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd.MM.uuuu");
String s = LocalDate.of(2024, 3, 15).format(f); // 15.03.2024 (то же, что f.format(date))
LocalDate d1 = LocalDate.parse("15.03.2024", f); // 2024-03-15
LocalDate d2 = f.parse("15.03.2024", LocalDate::from); // 2024-03-15
TemporalAccessor raw = f.parse("15.03.2024"); // набор полей, а не LocalDate
LocalDate.parse("15.03.2024 10:00", f); // DateTimeParseException: ... unparsed text found at index 10
Строка при разборе должна совпадать с шаблоном целиком. Методы withLocale(), withZone() и withResolverStyle() не меняют форматтер, а возвращают новый — результат нужно присвоить.
2. Какие готовые форматтеры есть в DateTimeFormatter и что они выводят?
Какие готовые форматтеры есть в DateTimeFormatter и что они выводят?
Для стандартных форматов есть константы ISO-8601 и не только. Их не нужно создавать, они потокобезопасны, и именно их используют parse(text) без форматтера.
LocalDateTime dt = LocalDateTime.of(2024, 3, 15, 14, 30, 45, 123_000_000);
ZonedDateTime zdt = dt.atZone(ZoneId.of("Europe/Moscow"));
DateTimeFormatter.ISO_LOCAL_DATE.format(dt); // 2024-03-15
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dt); // 2024-03-15T14:30:45.123
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zdt); // 2024-03-15T14:30:45.123+03:00
DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zdt); // 2024-03-15T14:30:45.123+03:00[Europe/Moscow]
DateTimeFormatter.ISO_INSTANT.format(zdt); // 2024-03-15T11:30:45.123Z
DateTimeFormatter.BASIC_ISO_DATE.format(dt); // 20240315
DateTimeFormatter.RFC_1123_DATE_TIME.format(zdt); // Fri, 15 Mar 2024 14:30:45 +0300
DateTimeFormatter.ISO_INSTANT.format(dt); // UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
ISO-форматтеры печатают дробную часть секунды только при её наличии и без лишних нулей, а при разборе принимают от 0 до 9 цифр. ISO_INSTANT требует момент времени, поэтому для LocalDateTime без зоны не работает. RFC_1123_DATE_TIME — формат HTTP-заголовков Date и Last-Modified, названия в нём всегда английские.
3. Как Locale влияет на DateTimeFormatter и чем MMMM отличается от LLLL?
Как Locale влияет на DateTimeFormatter и чем MMMM отличается от LLLL?
Locale задаёт язык названий месяцев и дней недели, маркер AM/PM, первый день недели и вид локализованных форматов. Её передают вторым аргументом ofPattern(pattern, locale) или через withLocale(); без неё используется Locale.getDefault(Locale.Category.FORMAT), и разбор «5 июня 2018» на сервере с en_US упадёт. В русском MMMM даёт родительный падеж, а LLLL — именительный:
Locale ru = Locale.forLanguageTag("ru");
LocalDate date = LocalDate.of(2024, 3, 15);
date.format(DateTimeFormatter.ofPattern("d MMMM yyyy", ru)); // 15 марта 2024
YearMonth.of(2024, 3).format(DateTimeFormatter.ofPattern("LLLL yyyy", ru)); // март 2024
YearMonth.of(2024, 3).format(DateTimeFormatter.ofPattern("MMMM yyyy", ru)); // марта 2024
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);
date.format(f.withLocale(ru)); // 15 марта 2024 г.
date.format(f.withLocale(Locale.US)); // March 15, 2024
Локализованные форматы (FULL, LONG, MEDIUM, SHORT) берутся из данных локалей JDK и меняются между версиями Java, поэтому для файлов, API и БД нужен явный шаблон или ISO-форматтер. ofLocalizedDateTime(FormatStyle.FULL) или LONG для LocalDateTime выбросит DateTimeException: Unable to extract ZoneId from temporal — эти стили печатают часовой пояс.
4. Как вывести и разобрать миллисекунды в DateTimeFormatter?
Как вывести и разобрать миллисекунды в DateTimeFormatter?
Буква S означает долю секунды, число букв — число цифр: SSS — миллисекунды, SSSSSS — микросекунды, SSSSSSSSS — наносекунды. Лишние цифры отбрасываются без округления. Буква n печатает наносекунды целым числом, поэтому для дробной части не подходит:
LocalTime t = LocalTime.of(14, 30, 45, 123_456_789);
t.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")); // 14:30:45.123
t.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSS")); // 14:30:45.123456
LocalTime.of(14, 30, 45, 5_000_000)
.format(DateTimeFormatter.ofPattern("HH:mm:ss.n")); // 14:30:45.5000000 (5 мс выглядят как полсекунды)
При разборе ss.SSS требует ровно три цифры: строка "2024-03-15 14:30:45.5" даст DateTimeParseException ... at index 20. Если число цифр заранее неизвестно, используйте appendFraction():
DateTimeFormatter flexible = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH:mm:ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.toFormatter();
LocalDateTime.parse("2024-03-15 14:30:45.5", flexible); // 2024-03-15T14:30:45.500
LocalDateTime.parse("2024-03-15 14:30:45", flexible); // 2024-03-15T14:30:45
В Java 8 шаблон без разделителей yyyyMMddHHmmssSSS не разбирает строку "20240315143045123" (JDK-8031085), с Java 9 это работает. Обход для Java 8 — appendPattern("yyyyMMddHHmmss") и appendValue(ChronoField.MILLI_OF_SECOND, 3).
5. Зачем нужен DateTimeFormatterBuilder и какие задачи он решает?
Зачем нужен DateTimeFormatterBuilder и какие задачи он решает?
DateTimeFormatterBuilder собирает форматтер по частям, когда одного ofPattern() не хватает: регистронезависимый разбор, необязательные части строки, значения по умолчанию, своя база для двузначного года, дробная часть переменной длины. В конце вызывается toFormatter().
DateTimeFormatter caseInsensitive = new DateTimeFormatterBuilder()
.parseCaseInsensitive() // до appendPattern, иначе не подействует
.appendPattern("dd MMM uuuu")
.toFormatter(Locale.ENGLISH);
LocalDate.parse("15 MAR 2024", caseInsensitive); // 2024-03-15 (без parseCaseInsensitive: ошибка at index 3)
DateTimeFormatter optionalTime = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd[ HH:mm]")
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.toFormatter();
LocalDateTime.parse("2024-03-15", optionalTime); // 2024-03-15T00:00
DateTimeFormatter twoDigitYear = new DateTimeFormatterBuilder()
.appendPattern("dd.MM.")
.appendValueReduced(ChronoField.YEAR, 2, 2, 1950)
.toFormatter();
LocalDate.parse("15.03.99", twoDigitYear); // 1999-03-15
LocalDate.parse("15.03.99", DateTimeFormatter.ofPattern("dd.MM.yy")); // 2099-03-15
Шаблон yy всегда трактует год как 2000–2099, поэтому для дат рождения из старых систем нужен appendValueReduced(). Квадратные скобки работают и в обычном ofPattern(), но без parseDefaulting() строку без времени нельзя разобрать в LocalDateTime.
6. Потокобезопасен ли DateTimeFormatter и чем он лучше SimpleDateFormat?
Потокобезопасен ли DateTimeFormatter и чем он лучше SimpleDateFormat?
Да. DateTimeFormatter неизменяем и потокобезопасен: один экземпляр можно хранить в static final поле и использовать из любого числа потоков без синхронизации.
public static final DateTimeFormatter RU_DATE = DateTimeFormatter.ofPattern("dd.MM.uuuu");
String s = LocalDate.of(2024, 3, 15).format(RU_DATE); // 15.03.2024
java.text.SimpleDateFormat хранит промежуточное состояние во внутреннем поле Calendar, поэтому общий экземпляр в нескольких потоках выдаёт неверные даты или падает с NumberFormatException. С ним приходилось создавать новый объект на каждый вызов или заворачивать его в ThreadLocal. У DateTimeFormatter методы withLocale(), withZone() и withResolverStyle() возвращают новый объект, поэтому общую константу нельзя случайно изменить из другого потока.