LocalDate, LocalTime и LocalDateTime: что это, работа с датой

Автор: IT Sectr Опубликовано: 2026-07-13 Время чтения: 12 мин

LocalDate, LocalTime и LocalDateTime — основные классы пакета java.time, предоставляющие работу с датой и временем без привязки к часовому поясу. По данным документации Oracle (Java 17, 2024), эти типы спроектированы как immutable и thread-safe, что делает их безопасными для многопоточных приложений. Они стали доступны на Android через desugaring начиная с API 26, а для более старых версий — через библиотеку ThreeTenABP.

Главное

  • LocalDate — immutable класс для представления даты (год, месяц, день) без времени и часового пояса.
  • LocalTime — immutable класс для представления времени (час, минута, секунда, наносекунда) без даты и часового пояса.
  • LocalDateTime — комбинация LocalDate и LocalTime, хранящая и дату, и время без привязки к зоне.
  • Все три класса поддерживают арифметические операции — добавление и вычитание дней, месяцев, часов через методы plus и minus.
  • На Android эти типы доступны через desugaring (API 26+) или библиотеку ThreeTenABP (API < 26).

Что такое LocalDate, LocalTime и LocalDateTime?

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

LocalDate хранит год в диапазоне от -999999999 до +999999999, месяц от 1 до 12 и день месяца с учётом високосных годов. Класс полностью immutable — любая операция возвращает новый объект.

LocalTime представляет время суток: часы, минуты, секунды и наносекунды. Максимальная точность — до наносекунды. LocalTime не содержит информации о дате и часовом поясе, что делает его удобным для хранения времени открытия магазина или длительности процесса.

LocalDateTime объединяет LocalDate и LocalTime в один объект. Это наиболее часто используемый тип, когда нужно хранить и дату, и время, но привязка к часовому поясу не требуется. Например, дата и время концерта в локальном формате.

По данным Oracle Java Documentation (2024), все три класса спроектированы на основе идей из библиотеки Joda-Time, но с улучшенной архитектурой и полной интеграцией в стандартную библиотеку.

Как устроен пакет java.time?

Пакет java.time появился в Java 8 как замена устаревшим классам Date, Calendar и SimpleDateFormat. Его архитектура построена на принципах immutable объектов и fluent-интерфейса.

Ключевая особенность — все основные классы являются value-based. Это означает, что их экземпляры сравниваются по значению, а не по ссылке, и их нельзя наследовать. Для сравнения двух объектов используется метод equals, а не оператор ==.

Пакет разделён на несколько категорий. Типы без часового пояса — LocalDate, LocalTime, LocalDateTime — используются для локальных дат и времени. Типы с часовым поясом — ZonedDateTime, OffsetDateTime, OffsetTime — добавляют информацию о смещении или зоне. Моментальные типы — Instant — представляют точку на временной шкале в UTC.

Такое разделение решает проблему, характерную для старого API: разработчик никогда не знал, содержит ли объект Date информацию о часовом поясе или нет. В java.time каждый тип явно декларирует свою семантику.

LocalDate: работа с датой

Класс LocalDate предоставляет множество методов для создания, чтения и модификации даты. Текущую дату можно получить через статический метод now(). Конкретную дату — через метод of(int year, int month, int dayOfMonth).

Для чтения компонентов даты используются геттеры: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). Метод getMonth() возвращает enum Month, а getDayOfWeek() — enum DayOfWeek.

LocalDate поддерживает проверку дат. Методы isBefore(), isAfter() и isEqual() позволяют сравнивать даты. Метод isLeapYear() проверяет, является ли год високосным. Метод lengthOfMonth() возвращает количество дней в месяце, а lengthOfYear() — в году.

Для модификации используются методы withYear(), withMonth(), withDayOfMonth(), которые возвращают новый объект с изменённым компонентом. Методы plusDays(), minusMonths() и аналогичные выполняют арифметику даты.

LocalTime: работа со временем

LocalTime представляет время суток с точностью до наносекунды. Стандартный формат — ISO-8601 (HH:mm:ss.nnnnnnnnn). Минимальное значение — 00:00, максимальное — 23:59:59.999999999.

Создать объект LocalTime можно через now() для текущего времени, of(int hour, int minute), of(int hour, int minute, int second) или of(int hour, int minute, int second, int nanoOfSecond). Метод parse(CharSequence text) парсит строку в формате ISO-8601.

Геттеры включают getHour(), getMinute(), getSecond(), getNano(). Метод toSecondOfDay() возвращает количество секунд от начала суток, а toNanoOfDay() — наносекунд. Это удобно для расчётов длительности внутри одного дня.

LocalTime поддерживает те же операции сравнения и модификации, что и LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). Методы isBefore() и isAfter() работают с учётом того, что время циклично в пределах суток.

LocalDateTime: комбинация даты и времени

LocalDateTime объединяет возможности LocalDate и LocalTime в одном классе. Он хранит и дату, и время, но без часового пояса. Это самый гибкий тип из локальных, но он требует осторожности при использовании в распределённых системах.

Создать LocalDateTime можно через статические методы now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) и их перегрузки. Также можно скомбинировать LocalDate и LocalTime через метод atTime().

LocalDateTime предоставляет доступ ко всем полям даты и времени через соответствующие геттеры: toLocalDate() и toLocalTime() возвращают отдельные компоненты. Метод truncatedTo(TemporalUnit unit) позволяет округлить время до заданной точности — например, до минут.

Для преобразования в часовой пояс используется метод atZone(ZoneId zone), который возвращает ZonedDateTime. Это единственный способ добавить часовой пояс к LocalDateTime.

Как создавать объекты даты и времени?

Все три класса используют единый шаблон создания через статические фабричные методы. Конструкторы классов объявлены как private — создать объект напрямую через new нельзя.

Основные способы создания:

  • now() — текущая дата/время из системных часов
  • of(...) — из компонентов (год, месяц, день и т.д.)
  • parse(String) — из строки в ISO-8601 формате
  • from(TemporalAccessor) — из другого temporal-объекта

Метод of имеет множество перегрузок. Для LocalDate нужны год, месяц и день. Для LocalTime — часы и минуты (опционально секунды и наносекунды). Для LocalDateTime — год, месяц, день, часы, минуты. Месяц можно передавать как int (1-12) или как enum Month.

kotlin
val today = LocalDate.now()
val specificDate = LocalDate.of(2026, Month.JULY, 21)
val parsedDate = LocalDate.parse("2026-07-21")

val currentTime = LocalTime.now()
val lunchTime = LocalTime.of(13, 30, 0)
val parsedTime = LocalTime.parse("13:30:00")

val now = LocalDateTime.now()
val meeting = LocalDateTime.of(2026, 7, 21, 15, 0)

Преобразование между типами

Классы java.time разработаны для удобного преобразования друг в друга. LocalDate может быть преобразован в LocalDateTime через метод atTime(LocalTime) или atStartOfDay(). LocalTime — через atDate(LocalDate).

LocalDateTime может быть преобразован обратно в LocalDate через toLocalDate() и в LocalTime через toLocalTime(). Для преобразования в ZonedDateTime используется метод atZone(ZoneId).

Преобразование в java.util.Date (для совместимости со старым кодом) требует промежуточного шага через Instant и часовой пояс. По данным Baeldung (2024), эта операция выполняется через Date.from(instant).

kotlin
val date = LocalDate.of(2026, 7, 21)
val dateTime = date.atTime(LocalTime.of(10, 30))

val time = LocalTime.of(14, 0)
val dateTimeFromTime = time.atDate(date)

val extractedDate = dateTime.toLocalDate()
val extractedTime = dateTime.toLocalTime()

val zoned = dateTime.atZone(ZoneId.of("Europe/Moscow"))

Форматирование и парсинг

Для форматирования и парсинга используется класс DateTimeFormatter. Он предоставляет предопределённые форматы через константы (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) и возможность создавать свои через pattern-строки.

Шаблоны форматирования используют символы: yyyy — год, MM — месяц (двузначный), dd — день, HH — час (0-23), mm — минута, ss — секунда. Метод format() вызывается на объекте даты-времени или через DateTimeFormatter.

DateTimeFormatter также поддерживает локализацию через статические методы ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle) и ofLocalizedDateTime(FormatStyle). Доступны стили SHORT, MEDIUM, LONG и FULL.

kotlin
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formatted = LocalDateTime.now().format(formatter)

val parsed = LocalDate.parse(
    "21.07.2026",
    DateTimeFormatter.ofPattern("dd.MM.yyyy")
)

Сравнение объектов даты-времени

Все три класса реализуют интерфейс Comparable, что позволяет сравнивать их естественным образом. Метод compareTo() возвращает отрицательное число, ноль или положительное в зависимости от порядка. Методы isBefore(), isAfter() и isEqual() возвращают boolean.

Для LocalDate сравнение идёт по хронологии — более ранняя дата считается меньшей. Для LocalTime — по времени суток. Для LocalDateTime — сначала по дате, затем по времени. Все сравнения корректно учитывают високосные годы и количество дней в месяцах.

Важное отличие от старого API: equals() для LocalDate, LocalTime и LocalDateTime сравнивает значения, а не ссылки. Это означает, что два объекта с одинаковыми полями будут равны, даже если это разные экземпляры.

kotlin
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)

if (d1.isBefore(d2)) {
    Log.d("Date", "d1 is before d2")
}

val sortedDates = listOf(d2, d1).sorted()

Арифметика даты и времени

Все три класса поддерживают арифметические операции через методы plus и minus. Для LocalDate доступны plusDays(), plusWeeks(), plusMonths(), plusYears() и аналогичные minus-методы. LocalTime поддерживает plusHours(), plusMinutes(), plusSeconds(), plusNanos().

LocalDateTime наследует все арифметические операции обоих типов. Особенность LocalDate: при добавлении месяца результаты корректно обрабатывают разную длину месяцев. Например, 31 января + 1 месяц = 28 (29 в високосный год) февраля.

Для более сложных операций существует класс Period (для дат) и Duration (для времени). Методы plus(TemporalAmount) и minus(TemporalAmount) принимают эти объекты.

kotlin
val today = LocalDate.now()
val nextWeek = today.plusDays(7)
val nextMonth = today.plusMonths(1)
val lastYear = today.minusYears(1)

val now = LocalTime.now()
val inTwoHours = now.plusHours(2)
val halfHourAgo = now.minusMinutes(30)

Примеры кода на Kotlin

Рассмотрим практический пример: приложение для учёта рабочих смен. Необходимо рассчитать продолжительность смены и определить, попадает ли она на ночное время. Используем LocalTime для времени начала и конца, LocalDate для даты и LocalDateTime для расчётов переходящих через полночь смен.

kotlin
data class Shift(
    val startTime: LocalTime,
    val endTime: LocalTime,
    val date: LocalDate
) {
    fun isOvernight(): Boolean = endTime.isBefore(startTime)

    fun durationInMinutes(): Long {
        val start = LocalDateTime.of(date, startTime)
        val end = LocalDateTime.of(
            if (isOvernight()) date.plusDays(1) else date,
            endTime
        )
        return Duration.between(start, end).toMinutes()
    }
}

Второй пример — расчёт возраста пользователя. Используем LocalDate для даты рождения и сравниваем с текущей датой, учитывая день и месяц рождения.

kotlin
fun calculateAge(birthDate: LocalDate): Int {
    val today = LocalDate.now()
    val period = Period.between(birthDate, today)
    return period.years
}

Третий пример — работа с уведомлениями. LocalDateTime используется для планирования напоминаний. Проверяем, наступило ли запланированное время.

kotlin
data class Reminder(
    val id: Long,
    val scheduledAt: LocalDateTime
) {
    fun isDue(): Boolean =
        LocalDateTime.now().isAfter(scheduledAt)
}

Поддержка в Android: API level и desugaring

Встроенная поддержка java.time появилась на Android начиная с API 26 (Android 8.0 Oreo). Для устройств с более старыми версиями Android необходимо использовать desugaring — механизм, который добавляет поддержку новых API Java в ранние версии.

Desugaring в Android Gradle Plugin настраивается через compileOptions в build.gradle. Достаточно указать isCoreLibraryDesugaringEnabled = true и добавить библиотеку desugar_jdk_libs. После этого java.time становится доступен для всех API level, начиная с 14.

Для проектов, которые не могут использовать desugaring (например, legacy проекты на AGP ниже 4.0), существует библиотека ThreeTenABP — backport java.time. Она предоставляет те же классы (LocalDate, LocalTime, LocalDateTime), но в пакете org.threeten.bp.

groovy
@Suppress("UnstableApiUsage")
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}

dependencies {
    "coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}

Типичные ошибки и как их избежать

Первая распространённая ошибка — использование LocalDateTime в распределённых системах без учёта часового пояса. Если сервер находится в Europe/Moscow, а клиент — в Asia/Tokyo, LocalDateTime будет интерпретироваться по-разному. Решение: использовать Instant или ZonedDateTime для глобальных данных.

Вторая ошибка — неправильный парсинг строк. По умолчанию LocalDate.parse() ожидает формат ISO-8601 (yyyy-MM-dd). Если строка в другом формате, нужно передать DateTimeFormatter явно. Также стоит обрабатывать DateTimeParseException, чтобы приложение не упало при некорректном вводе.

Третья ошибка — игнорирование null-безопасности. LocalDate, LocalTime и LocalDateTime — объекты, которые могут быть null. В Kotlin рекомендуется использовать nullable-типы с явной проверкой или Elvis-оператором. В Java — проверять на null перед вызовом методов.

Четвёртая ошибка — путаница между LocalDateTime и ZonedDateTime. LocalDateTime не содержит никакой информации о часовом поясе. Если нужно передать абсолютный момент времени — используйте зональные типы. Если достаточно локального времени — локальные.

Часто задаваемые вопросы

В чём разница между LocalDate и Date в Java?

Date хранит количество миллисекунд от 1970-01-01 UTC, а LocalDate хранит год, месяц и день без привязки к часовому поясу. Date мутабельный и не thread-safe, LocalDate — immutable и thread-safe. Date устарел начиная с Java 8.

Можно ли использовать LocalDateTime в базе данных?

Да, LocalDateTime хорошо маппится на SQL-тип TIMESTAMP WITHOUT TIME ZONE. JPA и Room поддерживают его через TypeConverter. Для TIMESTAMP WITH TIME ZONE используйте ZonedDateTime или OffsetDateTime.

Как получить количество дней между двумя датами?

Используйте ChronoUnit.DAYS.between(startDate, endDate). Этот метод возвращает long — разницу в днях. Для более детального расчёта используйте Period.between(), который возвращает Period с годами, месяцами и днями.

Что делать, если нужно сохранить точность времени до миллисекунд?

LocalTime поддерживает точность до наносекунд (9 знаков после запятой). Если нужна точность до миллисекунд, используйте truncateTo(ChronoUnit.MILLIS) перед сохранением. Это предотвращает проблемы с округлением при сериализации.

Почему LocalDate.now() возвращает разную дату на разных устройствах?

Метод now() использует системные часы устройства и часовой пояс по умолчанию. Если устройства находятся в разных часовых поясах, дата может отличаться. Для единой временной метки используйте Instant.now(), который всегда возвращает время в UTC.

Итоги

  • LocalDate — immutable класс для даты без времени и часового пояса. Используется для хранения дней рождения, сроков, дат событий.
  • LocalTime — immutable класс для времени суток с точностью до наносекунды. Подходит для хранения времени открытия, длительности процессов.
  • LocalDateTime — комбинация даты и времени без привязки к зоне. Самый гибкий локальный тип, но не подходит для распределённых систем.
  • Все три класса поддерживают арифметику, сравнение, форматирование и парсинг через единый API на основе DateTimeFormatter.
  • На Android java.time доступен через встроенную поддержку с API 26 или через desugaring для более старых версий.
  • Для глобальных меток времени и данных с часовыми поясами используйте ZonedDateTime или Instant вместо локальных типов.
  • При парсинге строк всегда передавайте DateTimeFormatter для нестандартных форматов и обрабатывайте DateTimeParseException.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также