LocalDate, LocalTime та LocalDateTime — основні класи пакета java.time, що надають роботу з датою та часом без прив’язки до часового поясу. За даними документації Oracle (Java 17, 2024), ці типи спроєктовані як immutable і thread-safe, що робить їх безпечними для багатопотокових застосунків. Вони стали доступні на Android через desugaring починаючи з API 26, а для старіших версій — через бібліотеку ThreeTenABP.
Головне
LocalDate — клас, що представляє дату у форматі рік-місяць-день без інформації про час та часовий пояс. Він використовується для зберігання таких даних, як день народження, дата події або термін дії.
LocalDate зберігає рік у діапазоні від -999999999 до +999999999, місяць від 1 до 12 і день місяця з урахуванням високосних років. Клас повністю immutable — будь-яка операція повертає новий об’єкт.
LocalTime представляє час доби: години, хвилини, секунди та наносекунди. Максимальна точність — до наносекунди. LocalTime не містить інформації про дату та часовий пояс, що робить його зручним для зберігання часу відкриття магазину або тривалості процесу.
LocalDateTime об’єднує LocalDate і LocalTime в один об’єкт. Це найбільш часто використовуваний тип, коли потрібно зберігати і дату, і час, але прив’язка до часового поясу не потрібна. Наприклад, дата та час концерту в локальному форматі.
За даними Oracle Java Documentation (2024), всі три класи спроєктовані на основі ідей з бібліотеки Joda-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 надає безліч методів для створення, читання та модифікації дати. Поточну дату можна отримати через статичний метод 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 представляє час доби з точністю до наносекунди. Стандартний формат — 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 об’єднує можливості 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 не можна.
Основні способи створення:
Метод of має безліч перевантажень. Для LocalDate потрібні рік, місяць і день. Для LocalTime — години та хвилини (опціонально секунди та наносекунди). Для LocalDateTime — рік, місяць, день, години, хвилини. Місяць можна передавати як int (1-12) або як enum Month.
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).
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.
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 порівнює значення, а не посилання. Це означає, що два об’єкти з однаковими полями будуть рівні, навіть якщо це різні екземпляри.
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)
if (d1.isBefore(d2)) {
Log.d("Дата", "d1 є раніше за 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) приймають ці об’єкти.
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)
Розглянемо практичний приклад: застосунок для обліку робочих змін. Необхідно розрахувати тривалість зміни та визначити, чи потрапляє вона на нічний час. Використовуємо LocalTime для часу початку та кінця, LocalDate для дати та LocalDateTime для розрахунків змін, що переходять через північ.
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 для дати народження та порівнюємо з поточною датою, враховуючи день і місяць народження.
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
Третій приклад — робота зі сповіщеннями. LocalDateTime використовується для планування нагадувань. Перевіряємо, чи настав запланований час.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
Вбудована підтримка 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.
@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 не містить жодної інформації про часовий пояс. Якщо потрібно передати абсолютний момент часу — використовуйте зональні типи. Якщо достатньо локального часу — локальні.
Часті запитання
Date зберігає кількість мілісекунд від 1970-01-01 UTC, а LocalDate зберігає рік, місяць і день без прив’язки до часового поясу. Date мутабельний і не thread-safe, LocalDate — immutable і thread-safe. Date застарів починаючи з Java 8.
Так, 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) перед збереженням. Це запобігає проблемам з округленням при серіалізації.
Метод now() використовує системні годинники пристрою та часовий пояс за замовчуванням. Якщо пристрої знаходяться в різних часових поясах, дата може відрізнятися. Для єдиної часової мітки використовуйте Instant.now(), який завжди повертає час у UTC.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також