Класс LocalDateTime в Java: создание, форматирование и примеры
Вызов LocalDateTime.parse("2024-03-15 10:30") выглядит безобидно, но падает с DateTimeParseException: Text '2024-03-15 10:30' could not be parsed at index 10. На десятой позиции стандартный формат ISO-8601 ждёт букву T, а не пробел: LocalDateTime.parse("2024-03-15T10:30") работает, а для формата с пробелом нужен свой DateTimeFormatter.
LocalDateTime в Java (java.time.LocalDateTime) — неизменяемый потокобезопасный класс, который хранит дату и время без часового пояса: год, месяц, день, часы, минуты, секунды и наносекунды, например 2024-03-15T14:30:45.123. По сути это LocalDate и LocalTime в одном объекте. Класс появился в Java 8 в пакете java.time; текущие дату и время возвращает LocalDateTime.now(), конкретные — LocalDateTime.of().
Как создать LocalDateTime: of(), atTime(), parse()
Публичного конструктора у LocalDateTime нет — объект создают фабричными методами. LocalDateTime.of(год, месяц, день, часы, минуты[, секунды[, наносекунды]]) задаёт значение по частям, LocalDateTime.of(LocalDate, LocalTime) склеивает готовые дату и время, date.atTime() и time.atDate() делают то же со стороны LocalDate и LocalTime, а parse() разбирает строку ISO-8601.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
public class LocalDateTimeExample1 {
public static void main(String[] args) {
// Секунды и наносекунды равны нулю
LocalDateTime date1 = LocalDateTime.of(2014, 9, 19, 14, 5);
// Наносекунды равны нулю
LocalDateTime date2 = LocalDateTime.of(2014, 9, 19, 14, 5, 20);
LocalDateTime date3 = LocalDateTime.of(2014, 9, 19, 14, 5, 20, 9);
LocalDateTime date4 = LocalDateTime.of(2024, Month.MARCH, 15, 14, 30);
LocalDate date = LocalDate.of(2024, 3, 15);
LocalTime time = LocalTime.of(9, 15);
LocalDateTime date5 = date.atTime(14, 30, 59);
LocalDateTime date6 = time.atDate(date);
LocalDateTime date7 = LocalDateTime.of(date, time);
LocalDateTime date8 = date.atStartOfDay();
LocalDateTime date9 = LocalDateTime.parse("2024-03-15T14:30:45");
System.out.println(date1);
System.out.println(date2);
System.out.println(date3);
System.out.println(date4);
System.out.println(date5);
System.out.println(date6);
System.out.println(date7);
System.out.println(date8);
System.out.println(date9);
}
} 2014-09-19T14:05
2014-09-19T14:05:20
2014-09-19T14:05:20.000000009
2024-03-15T14:30
2024-03-15T14:30:59
2024-03-15T09:15
2024-03-15T09:15
2024-03-15T00:00
2024-03-15T14:30:45 Обратите внимание на формат вывода: между датой и временем стоит буква T, а нулевые секунды и наносекунды toString() не печатает — поэтому date1 выглядит как 2014-09-19T14:05. Девять наносекунд в date3 выводятся полностью, девятью цифрами.
Все значения проверяются сразу, при создании объекта. Несуществующая дата или время вызывает исключение, а строка без T или без времени не разбирается:
LocalDateTime.of(2024, 2, 30, 10, 0); // DateTimeException: Invalid date 'FEBRUARY 30'
LocalDateTime.of(2024, 3, 15, 24, 0); // DateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 24
LocalDateTime.parse("2024-03-15 10:30"); // DateTimeParseException: Text '2024-03-15 10:30' could not be parsed at index 10
LocalDateTime.parse("2024-03-15"); // DateTimeParseException: Text '2024-03-15' could not be parsed at index 10 Строку только с датой превращают в LocalDateTime в два шага: LocalDate.parse("2024-03-15").atStartOfDay(). Как разбирать другие форматы — в разделе Форматирование LocalDateTime.
LocalDateTime.now(): текущие дата и время
Метод LocalDateTime.now() возвращает текущие дату и время по системным часам в часовом поясе JVM по умолчанию. Вариант LocalDateTime.now(ZoneId) возвращает дату и время в указанной зоне, а now(Clock) берёт их из объекта Clock, что удобно для тестов.
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class LocalDateTimeNowExample {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime utc = LocalDateTime.now(ZoneOffset.UTC);
System.out.println("Сейчас: " + now);
System.out.println("В UTC: " + utc);
}
} Сейчас: 2026-10-19T14:35:07.123456 (зависит от момента запуска и зоны JVM, здесь Europe/Moscow)
В UTC: 2026-10-19T11:35:07.123512 Количество знаков после секунд зависит от версии Java и ОС: в Java 8 обычно миллисекунды, начиная с Java 9 — как правило микросекунды. Сам результат now() не помнит, в какой зоне он получен: это просто «показания часов на стене». Почему на сервере now() может отличаться от вашего компьютера — в FAQ урока Дата и время в Java, пример с фиксированным Clock — в уроке LocalTime (для LocalDateTime.now(clock) всё так же).
Получение даты, времени и отдельных полей
Поля LocalDateTime читают геттерами: getYear(), getMonthValue(), getMonth(), getDayOfMonth(), getDayOfYear(), getDayOfWeek(), getHour(), getMinute(), getSecond(), getNano(). Дату целиком возвращает toLocalDate(), время — toLocalTime().
import java.time.LocalDateTime;
public class LocalDateTimeExample2 {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2024, 3, 15, 14, 30, 45);
int year = dateTime.getYear();
int dayYear = dateTime.getDayOfYear();
int hour = dateTime.getHour();
int minute = dateTime.getMinute();
System.out.println("Год: " + year);
System.out.println("Месяц: " + dateTime.getMonth());
System.out.println("День в году: " + dayYear);
System.out.println("День недели: " + dateTime.getDayOfWeek());
System.out.println("Часы: " + hour);
System.out.println("Минуты: " + minute);
System.out.println("Дата: " + dateTime.toLocalDate());
System.out.println("Время: " + dateTime.toLocalTime());
}
} Год: 2024
Месяц: MARCH
День в году: 75
День недели: FRIDAY
Часы: 14
Минуты: 30
Дата: 2024-03-15
Время: 14:30:45 Работает и универсальный get(ChronoField): dateTime.get(ChronoField.MINUTE_OF_DAY) вернёт 870. У LocalDateTime есть все поля даты и времени, но нет полей зоны: dateTime.get(ChronoField.OFFSET_SECONDS) выбросит UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds. Подробно о полях ChronoField — в уроках LocalDate и LocalTime.
Изменение, сложение и сравнение: with(), plus(), isAfter()
Методы with...() заменяют одно поле, plus...() и minus...() сдвигают значение на годы, месяцы, недели, дни, часы, минуты, секунды или наносекунды, а isAfter(), isBefore(), isEqual() сравнивают два момента. Объект неизменяем: каждый метод возвращает новый LocalDateTime, исходный остаётся прежним.
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
public class LocalDateTimeExample3 {
public static void main(String[] args) {
LocalDateTime base = LocalDateTime.of(2024, 3, 15, 14, 30);
LocalDateTime date1 = base.with(ChronoField.HOUR_OF_DAY, 10);
LocalDateTime date2 = base.withMonth(8);
LocalDateTime date3 = base.withYear(2013).withMinute(0);
// Сравнение
boolean after = base.isAfter(date1);
boolean before = base.isBefore(date1);
boolean equal = base.equals(date1);
// Добавление
LocalDateTime date4 = base.plusYears(4);
LocalDateTime date5 = base.plusWeeks(3);
LocalDateTime date6 = date1.plus(2, ChronoUnit.HOURS);
// Вычитание
LocalDateTime date7 = base.minusMonths(2);
LocalDateTime date8 = base.minusNanos(1);
LocalDateTime date9 = base.minus(10, ChronoUnit.SECONDS);
System.out.println("date1: " + date1);
System.out.println("date2: " + date2);
System.out.println("date3: " + date3);
System.out.println("after: " + after + ", before: " + before + ", equal: " + equal);
System.out.println("date4: " + date4);
System.out.println("date5: " + date5);
System.out.println("date6: " + date6);
System.out.println("date7: " + date7);
System.out.println("date8: " + date8);
System.out.println("date9: " + date9);
System.out.println("base не изменился: " + base);
}
} date1: 2024-03-15T10:30
date2: 2024-08-15T14:30
date3: 2013-03-15T14:00
after: true, before: false, equal: false
date4: 2028-03-15T14:30
date5: 2024-04-05T14:30
date6: 2024-03-15T12:30
date7: 2024-01-15T14:30
date8: 2024-03-15T14:29:59.999999999
date9: 2024-03-15T14:29:50
base не изменился: 2024-03-15T14:30 В отличие от LocalTime, время здесь не «заворачивается» внутри суток: LocalDateTime.of(2024, 3, 15, 23, 30).plusMinutes(45) даст 2024-03-16T00:15 — дата сдвинется на следующий день. Для сортировки используйте compareTo() (класс реализует Comparable) и сравнивайте результат с нулём, а не с -1: сначала сравниваются даты, и разница может быть любым числом.
Чтобы отбросить лишнюю точность, есть truncatedTo(): dateTime.truncatedTo(ChronoUnit.DAYS) даёт полночь того же дня, truncatedTo(ChronoUnit.MINUTES) — значение без секунд. Единицы больше суток не поддерживаются: truncatedTo(ChronoUnit.MONTHS) выбросит UnsupportedTemporalTypeException: Unit is too large to be used for truncation.
Форматирование LocalDateTime: format() и parse()
Чтобы вывести LocalDateTime в нужном виде, например 15.03.2024 14:30, вызовите dateTime.format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")). Обратная операция — LocalDateTime.parse(текст, formatter). Без форматтера toString() и parse() работают только с ISO-8601: 2024-03-15T14:30.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FormatLocalDateTimeExample {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2024, 3, 15, 14, 30);
DateTimeFormatter ru = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
DateTimeFormatter sql = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(dateTime); // toString(), ISO-8601
System.out.println(dateTime.format(ru));
System.out.println(dateTime.format(sql));
// Строка -> LocalDateTime
LocalDateTime parsed = LocalDateTime.parse("15.03.2024 14:30", ru);
LocalDateTime withSpace = LocalDateTime.parse("2024-03-15 10:30:00", sql);
System.out.println(parsed);
System.out.println(withSpace);
}
} 2024-03-15T14:30
15.03.2024 14:30
2024-03-15 14:30:00
2024-03-15T14:30
2024-03-15T10:30 Два частых сбоя при форматировании. Если в шаблоне нет времени, а разбираете вы в LocalDateTime, например LocalDateTime.parse("15.03.2024", DateTimeFormatter.ofPattern("dd.MM.yyyy")), получите DateTimeParseException: ... Unable to obtain LocalDateTime from TemporalAccessor — разбирайте в LocalDate и вызывайте atStartOfDay(). А шаблон со смещением зоны (XXX, Z) на LocalDateTime выбросит UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds: зоны в объекте нет, сначала нужен atZone(). Все символы шаблона и работа с Locale — в уроке Класс DateTimeFormatter.
Разница между двумя LocalDateTime
Разницу между двумя LocalDateTime считают через Duration.between(start, end) — результат в часах, минутах и секундах — или через ChronoUnit.X.between(start, end), который возвращает long в нужных единицах. Учитываются только полные единицы: неполный день или час отбрасывается.
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class BetweenLocalDateTimeExample {
public static void main(String[] args) {
LocalDateTime start = LocalDateTime.of(2024, 3, 15, 22, 0);
LocalDateTime end = LocalDateTime.of(2024, 3, 16, 6, 30);
System.out.println(Duration.between(start, end));
System.out.println(ChronoUnit.MINUTES.between(start, end));
System.out.println(ChronoUnit.DAYS.between(start, end));
System.out.println(Period.between(start.toLocalDate(), end.toLocalDate()));
}
} PT8H30M
510
0
P1D Последние две строки отвечают на разные вопросы. ChronoUnit.DAYS.between считает полные сутки (прошло 8,5 часа — значит, 0 дней), а Period.between по датам видит, что календарный день сменился. Сам Period.between(start, end) с аргументами LocalDateTime не скомпилируется: он принимает только LocalDate. Подробнее — в уроках Класс Duration и Класс Period.
LocalDateTime vs Instant: что выбрать
LocalDateTime — это дата и время «на часах», без зоны, поэтому одно и то же значение в Москве и в Берлине соответствует разным моментам. Instant — точный момент на шкале UTC, одинаковый для всех. Для меток событий (создание заказа, логи) выбирайте Instant, для локальных значений вроде «приём у врача 15 марта в 10:30» — LocalDateTime.
| Признак | LocalDateTime | Instant |
|---|---|---|
| Что хранит | Год, месяц, день, часы, минуты, секунды, наносекунды | Секунды и наносекунды от 1970-01-01T00:00:00Z |
| Часовой пояс | Нет и не подразумевается | Нет, но значение всегда в UTC |
Пример toString() | 2024-03-15T10:30 | 2024-03-15T07:30:00Z |
| Определяет момент времени? | Нет, пока не указана зона | Да |
| Перевод часов на летнее время | Не учитывает | Не касается: шкала UTC непрерывна |
Поля «год», «месяц», plusMonths() | Есть | Нет: только секунды, миллисекунды, наносекунды (и дни как 86400 секунд) |
| Когда выбирать | Расписание, дата и время в чеке, поле формы «дата и время» | Метки времени в логах и БД, обмен между сервисами |
Что выведет этот код? Отсутствие зоны у LocalDateTime хорошо видно на переводе часов:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
public class DstExample {
public static void main(String[] args) {
ZoneId berlin = ZoneId.of("Europe/Berlin");
// 31 марта 2024: в Берлине часы переводят с 02:00 сразу на 03:00
LocalDateTime gap = LocalDateTime.of(2024, 3, 31, 2, 30);
System.out.println(gap.atZone(berlin));
// 27 октября 2024: с 03:00 обратно на 02:00, ночь длиннее на час
LocalDateTime start = LocalDateTime.of(2024, 10, 27, 0, 0);
LocalDateTime end = LocalDateTime.of(2024, 10, 27, 6, 0);
System.out.println(ChronoUnit.HOURS.between(start, end));
System.out.println(ChronoUnit.HOURS.between(start.atZone(berlin), end.atZone(berlin)));
}
} 2024-03-31T03:30+02:00[Europe/Berlin]
6
7 Время 02:30 31 марта в Берлине не существует, но LocalDateTime об этом не знает — при привязке к зоне оно молча сдвигается на 03:30. А в ночь обратного перевода между 00:00 и 06:00 по часам прошло 6 часов, хотя на самом деле — 7. Если считаете длительность смен, тарифицируете звонки или храните моменты событий, работайте с ZonedDateTime или Instant.
Конвертация в Instant, ZonedDateTime и миллисекунды
Чтобы превратить LocalDateTime в Instant или миллисекунды от эпохи, обязательно укажите зону: ldt.atZone(zone).toInstant() или смещение: ldt.toInstant(ZoneOffset.UTC). Обратная операция — LocalDateTime.ofInstant(instant, zone). Без зоны преобразование невозможно, поэтому прямого метода toInstant() без аргументов у класса нет.
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
public class ConvertLocalDateTimeExample {
public static void main(String[] args) {
LocalDateTime meeting = LocalDateTime.of(2024, 3, 15, 10, 30);
ZoneId moscow = ZoneId.of("Europe/Moscow");
ZonedDateTime zoned = meeting.atZone(moscow);
Instant inMoscow = zoned.toInstant();
Instant inBerlin = meeting.atZone(ZoneId.of("Europe/Berlin")).toInstant();
Instant asUtc = meeting.toInstant(ZoneOffset.UTC);
long millis = inMoscow.toEpochMilli();
LocalDateTime inTokyo = LocalDateTime.ofInstant(inMoscow, ZoneId.of("Asia/Tokyo"));
System.out.println(zoned);
System.out.println(inMoscow);
System.out.println(inBerlin);
System.out.println(asUtc);
System.out.println(millis);
System.out.println(inTokyo);
System.out.println(zoned.toLocalDateTime());
}
} 2024-03-15T10:30+03:00[Europe/Moscow]
2024-03-15T07:30:00Z
2024-03-15T09:30:00Z
2024-03-15T10:30:00Z
1710487800000
2024-03-15T16:30
2024-03-15T10:30 Строки 2–4 показывают главное: одна и та же запись «15 марта, 10:30» даёт три разных момента в зависимости от зоны. Миллисекунды без объекта Instant можно получить и короче — meeting.toEpochSecond(ZoneOffset.ofHours(3)) вернёт секунды. Конвертация в старый java.util.Date идёт через тот же Instant: Date.from(ldt.atZone(zone).toInstant()); полная таблица переходов со старого API — в уроке Дата и время в Java.
ZoneId.systemDefault() — скрытая зависимость
Код ldt.atZone(ZoneId.systemDefault()) на ноутбуке разработчика в Москве и на сервере в UTC даст моменты с разницей в 3 часа. Если значение пришло от пользователя, берите его зону (из профиля или браузера), а не зону JVM. Если зона неизвестна, сохраните LocalDateTime как есть и не превращайте его в Instant наугад.
Шпаргалка по методам LocalDateTime
Методы LocalDateTime делятся на группы: создание, чтение полей, изменение копии, сравнение, форматирование и привязка к зоне. Все методы, которые «меняют» значение, возвращают новый объект.
| Метод | Что делает | Граничный случай / ошибка |
|---|---|---|
of(y, m, d, h, min[, s[, n]]), of(LocalDate, LocalTime) | Дата и время из частей | of(2024, 2, 30, 10, 0) — DateTimeException |
now(), now(ZoneId), now(Clock) | Текущие дата и время | Без аргумента берётся зона JVM по умолчанию |
parse(text), parse(text, formatter) | Разбор строки | Пробел вместо T — DateTimeParseException at index 10 |
getYear() … getNano(), toLocalDate(), toLocalTime() | Поля, отдельно дата или время | get(ChronoField.OFFSET_SECONDS) — UnsupportedTemporalTypeException |
withYear() … withNano(), with(ChronoField, value) | Копия с заменённым полем | withHour(24) — DateTimeException |
plusYears() … plusNanos(), minus...(), plus(n, ChronoUnit) | Сдвиг на N единиц | Переход через полночь меняет дату |
isBefore(), isAfter(), isEqual(), compareTo() | Сравнение | compareTo() сравнивайте с нулём, не с -1 |
truncatedTo(unit) | Обнуляет поля младше единицы | MONTHS и больше — UnsupportedTemporalTypeException |
format(formatter) | Строка по шаблону | Шаблон с XXX — Unsupported field: OffsetSeconds |
atZone(ZoneId), atOffset(ZoneOffset) | Привязка к зоне: ZonedDateTime / OffsetDateTime | Время в «дыре» перевода часов сдвигается вперёд |
toInstant(ZoneOffset), toEpochSecond(ZoneOffset), ofInstant(Instant, ZoneId) | Переход к моменту времени и обратно | Без зоны или смещения не вызвать |
Где чаще всего спотыкаются
Ошибки с LocalDateTime чаще всего связаны с форматом строки (буква T), забытым присваиванием результата и ожиданием, что у значения есть часовой пояс. Ниже — типичные случаи и исправления.
| Ошибка | Что происходит | Как правильно |
|---|---|---|
dt.plusHours(1); без присваивания | Результат потерян, dt не меняется | dt = dt.plusHours(1); |
LocalDateTime.parse("2024-03-15 10:30") | DateTimeParseException at index 10 | "2024-03-15T10:30" или ofPattern("yyyy-MM-dd HH:mm") |
createdAt = LocalDateTime.now() на серверах в разных зонах | Время событий нельзя сравнить и упорядочить | Instant.now() или OffsetDateTime |
Period.between(ldt1, ldt2) | Не компилируется: нужен LocalDate | Duration.between(ldt1, ldt2) или Period.between(ldt1.toLocalDate(), ldt2.toLocalDate()) |
ChronoUnit.DAYS.between для «сменилось ли число» | 22:00 → 06:30 следующего дня даёт 0 | Сравнивайте toLocalDate() |
dt1 == dt2 | Сравниваются ссылки, а не значения | dt1.isEqual(dt2) или dt1.equals(dt2) |
date.atTime(14, 30, 59, 999999) | Получится 14:30:59.000999999: четвёртый аргумент — наносекунды | 999_999_999; подробнее — в уроке LocalTime |
Часто задаваемые вопросы
Как сохранить LocalDateTime в базу данных?
Начиная с JDBC 4.2 передавайте объект напрямую: ps.setObject(1, dateTime) и rs.getObject("created", LocalDateTime.class); подходящий тип столбца — TIMESTAMP (без часового пояса). Hibernate 5.2+ и JPA 2.2 отображают LocalDateTime сами. В старом коде используется Timestamp.valueOf(dateTime) и timestamp.toLocalDateTime(). Если в столбце должен храниться момент события, выбирайте TIMESTAMP WITH TIME ZONE и OffsetDateTime или Instant.
Как получить начало и конец дня в LocalDateTime?
Начало дня — date.atStartOfDay() (00:00), конец — date.atTime(LocalTime.MAX) (23:59:59.999999999). Для выборок за день надёжнее полуоткрытый интервал: created >= date.atStartOfDay() и created < date.plusDays(1).atStartOfDay() — так не потеряются записи с долями секунды, если база хранит другую точность.
Почему Jackson не сериализует LocalDateTime в JSON?
Поддержка java.time вынесена в модуль jackson-datatype-jsr310. Без него Jackson 2.12+ выбрасывает InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default. Зарегистрируйте JavaTimeModule и отключите SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, иначе значение запишется массивом чисел вроде [2024,3,15,14,30], а не строкой ISO-8601. Spring Boot делает обе настройки автоматически; свой формат задают аннотацией @JsonFormat(pattern = "dd.MM.yyyy HH:mm").
Как отсортировать список объектов по полю LocalDateTime?
LocalDateTime реализует Comparable, поэтому достаточно orders.sort(Comparator.comparing(Order::getCreatedAt)); новые сверху — добавьте .reversed(). Если поле может быть null, обычный компаратор упадёт с NullPointerException — используйте Comparator.comparing(Order::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).
Practice
Reinforce this lesson with hands-on tasks, each with a level and a time estimate.
All module tasks →Video Explanation
Prefer video format? Watch this lesson with examples and explanations.
Comments