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).
За четене на компонентите на датата се използват getterи: 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.
Getterите включват 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 осигурява достъп до всички полета на дата и час чрез съответните getterи: 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) и възможност за създаване на собствени формати чрез шаблони.
Шаблоните за форматиране използват символи: 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 от 14 нататък.
За проекти, които не могат да използват desugaring (например, стари проекти на 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също