LocalDate, LocalTime i LocalDateTime — podstawowe klasy pakietu java.time, zapewniające pracę z datą i czasem bez przywiązania do strefy czasowej. Według dokumentacji Oracle (Java 17, 2024), te typy są zaprojektowane jako immutable i thread-safe, co czyni je bezpiecznymi dla aplikacji wielowątkowych. Stały się dostępne na Androidzie poprzez desugaring od API 26, a dla starszych wersji — przez bibliotekę ThreeTenABP.
Najważniejsze
LocalDate — klasa reprezentująca datę w formacie rok-miesiąc-dzień bez informacji o czasie i strefie czasowej. Jest używana do przechowywania takich danych jak data urodzenia, data wydarzenia czy termin ważności.
LocalDate przechowuje rok w zakresie od -999999999 do +999999999, miesiąc od 1 do 12 i dzień miesiąca z uwzględnieniem lat przestępnych. Klasa jest w pełni immutable — każda operacja zwraca nowy obiekt.
LocalTime reprezentuje porę dnia: godziny, minuty, sekundy i nanosekundy. Maksymalna precyzja — do nanosekundy. LocalTime nie zawiera informacji o dacie i strefie czasowej, co czyni go wygodnym do przechowywania godziny otwarcia sklepu lub czasu trwania procesu.
LocalDateTime łączy LocalDate i LocalTime w jeden obiekt. To najczęściej używany typ, gdy potrzebujemy przechowywać datę i czas, ale przywiązanie do strefy czasowej nie jest wymagane. Na przykład data i godzina koncertu w formacie lokalnym.
Według Oracle Java Documentation (2024), wszystkie trzy klasy są zaprojektowane w oparciu o pomysły z biblioteki Joda-Time, ale z ulepszoną architekturą i pełną integracją w standardowej bibliotece.
Pakiet java.time pojawił się w Java 8 jako zamiennik przestarzałych klas Date, Calendar i SimpleDateFormat. Jego architektura opiera się na zasadach immutable obiektów i fluent-interfejsu.
Kluczowa cecha — wszystkie główne klasy są value-based. Oznacza to, że ich instancje są porównywane według wartości, a nie referencji, i nie można ich dziedziczyć. Do porównywania dwóch obiektów używa się metody equals, a nie operatora ==.
Pakiet jest podzielony na kilka kategorii. Typy bez strefy czasowej — LocalDate, LocalTime, LocalDateTime — są używane dla lokalnych dat i czasu. Typy ze strefą czasową — ZonedDateTime, OffsetDateTime, OffsetTime — dodają informację o przesunięciu lub strefie. Typy momentalne — Instant — reprezentują punkt na osi czasu w UTC.
Takie rozdzielenie rozwiązuje problem charakterystyczny dla starego API: programista nigdy nie wiedział, czy obiekt Date zawiera informację o strefie czasowej, czy nie. W java.time każdy typ jawnie deklaruje swoją semantykę.
Klasa LocalDate udostępnia wiele metod do tworzenia, odczytu i modyfikacji daty. Bieżącą datę można uzyskać za pomocą metody statycznej now(). Konkretną datę — za pomocą metody of(int year, int month, int dayOfMonth).
Do odczytu komponentów daty używane są gettery: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). Metoda getMonth() zwraca enum Month, a getDayOfWeek() — enum DayOfWeek.
LocalDate obsługuje sprawdzanie dat. Metody isBefore(), isAfter() i isEqual() pozwalają porównywać daty. Metoda isLeapYear() sprawdza, czy rok jest przestępny. Metoda lengthOfMonth() zwraca liczbę dni w miesiącu, a lengthOfYear() — w roku.
Do modyfikacji używane są metody withYear(), withMonth(), withDayOfMonth(), które zwracają nowy obiekt ze zmienionym komponentem. Metody plusDays(), minusMonths() i analogiczne wykonują arytmetykę daty.
LocalTime reprezentuje porę dnia z precyzją do nanosekundy. Standardowy format — ISO-8601 (HH:mm:ss.nnnnnnnnn). Minimalna wartość — 00:00, maksymalna — 23:59:59.999999999.
Można utworzyć obiekt LocalTime przez now() dla bieżącego czasu, of(int hour, int minute), of(int hour, int minute, int second) lub of(int hour, int minute, int second, int nanoOfSecond). Metoda parse(CharSequence text) parsuje ciąg w formacie ISO-8601.
Gettery obejmują getHour(), getMinute(), getSecond(), getNano(). Metoda toSecondOfDay() zwraca liczbę sekund od początku dnia, a toNanoOfDay() — nanosekund. Jest to wygodne do obliczeń czasu trwania w ciągu jednego dnia.
LocalTime obsługuje te same operacje porównywania i modyfikacji co LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). Metody isBefore() i isAfter() działają z uwzględnieniem cykliczności czasu w ciągu doby.
LocalDateTime łączy możliwości LocalDate i LocalTime w jednej klasie. Przechowuje datę i czas, ale bez strefy czasowej. To najbardziej elastyczny typ z lokalnych, ale wymaga ostrożności przy użyciu w systemach rozproszonych.
Można utworzyć LocalDateTime za pomocą metod statycznych now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) i ich przeciążeń. Można również połączyć LocalDate i LocalTime za pomocą metody atTime().
LocalDateTime zapewnia dostęp do wszystkich pól daty i czasu za pomocą odpowiednich getterów: toLocalDate() i toLocalTime() zwracają poszczególne komponenty. Metoda truncatedTo(TemporalUnit unit) pozwala zaokrąglić czas do zadanej precyzji — na przykład do minut.
Do konwersji na strefę czasową używana jest metoda atZone(ZoneId zone), która zwraca ZonedDateTime. To jedyny sposób na dodanie strefy czasowej do LocalDateTime.
Wszystkie trzy klasy używają jednolitego wzorca tworzenia za pomocą statycznych metod fabrycznych. Konstruktory klas są zadeklarowane jako private — nie można utworzyć obiektu bezpośrednio przez new.
Podstawowe sposoby tworzenia:
Metoda of ma wiele przeciążeń. Dla LocalDate potrzebne są rok, miesiąc i dzień. Dla LocalTime — godziny i minuty (opcjonalnie sekundy i nanosekundy). Dla LocalDateTime — rok, miesiąc, dzień, godziny, minuty. Miesiąc można przekazać jako int (1-12) lub jako 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)
Klasy java.time zostały zaprojektowane do wygodnej konwersji między sobą. LocalDate może być przekształcony na LocalDateTime za pomocą metody atTime(LocalTime) lub atStartOfDay(). LocalTime — za pomocą atDate(LocalDate).
LocalDateTime może być przekształcony z powrotem na LocalDate przez toLocalDate() i na LocalTime przez toLocalTime(). Do konwersji na ZonedDateTime używana jest metoda atZone(ZoneId).
Konwersja na java.util.Date (dla kompatybilności ze starym kodem) wymaga pośredniego kroku przez Instant i strefę czasową. Według Baeldung (2024), ta operacja jest wykonywana przez 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"))
Do formatowania i parsowania używana jest klasa DateTimeFormatter. Udostępnia ona predefiniowane formaty przez stałe (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) oraz możliwość tworzenia własnych za pomocą wzorców.
Wzorce formatowania używają symboli: yyyy — rok, MM — miesiąc (dwucyfrowy), dd — dzień, HH — godzina (0-23), mm — minuta, ss — sekunda. Metoda format() jest wywoływana na obiekcie daty-czasu lub za pomocą DateTimeFormatter.
DateTimeFormatter obsługuje również lokalizację za pomocą metod statycznych ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle) i ofLocalizedDateTime(FormatStyle). Dostępne są style SHORT, MEDIUM, LONG i 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")
)
Wszystkie trzy klasy implementują interfejs Comparable, co pozwala na naturalne porównywanie. Metoda compareTo() zwraca liczbę ujemną, zero lub dodatnią w zależności od kolejności. Metody isBefore(), isAfter() i isEqual() zwracają boolean.
Dla LocalDate porównanie odbywa się chronologicznie — wcześniejsza data jest mniejsza. Dla LocalTime — według pory dnia. Dla LocalDateTime — najpierw według daty, potem według czasu. Wszystkie porównania poprawnie uwzględniają lata przestępne i liczbę dni w miesiącach.
Ważna różnica w stosunku do starego API: equals() dla LocalDate, LocalTime i LocalDateTime porównuje wartości, a nie referencje. Oznacza to, że dwa obiekty z tymi samymi polami będą równe, nawet jeśli są to różne instancje.
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)
if (d1.isBefore(d2)) {
Log.d("Data", "d1 jest przed d2")
}
val sortedDates = listOf(d2, d1).sorted()
Wszystkie trzy klasy obsługują operacje arytmetyczne za pomocą metod plus i minus. Dla LocalDate dostępne są plusDays(), plusWeeks(), plusMonths(), plusYears() i analogiczne minus-metody. LocalTime obsługuje plusHours(), plusMinutes(), plusSeconds(), plusNanos().
LocalDateTime dziedziczy wszystkie operacje arytmetyczne obu typów. Cecha LocalDate: przy dodawaniu miesiąca wyniki poprawnie uwzględniają różną długość miesięcy. Na przykład 31 stycznia + 1 miesiąc = 28 (29 w roku przestępnym) lutego.
Do bardziej złożonych operacji istnieją klasy Period (dla dat) i Duration (dla czasu). Metody plus(TemporalAmount) i minus(TemporalAmount) przyjmują te obiekty.
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)
Rozwaźmy praktyczny przykład: aplikacja do ewidencji zmian roboczych. Należy obliczyć czas trwania zmiany i określić, czy przypada na porę nocną. Używamy LocalTime dla czasu rozpoczęcia i zakończenia, LocalDate dla daty i LocalDateTime dla obliczeń zmian przechodzących przez północ.
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()
}
}
Drugi przykład — obliczanie wieku użytkownika. Używamy LocalDate dla daty urodzenia i porównujemy z bieżącą datą, uwzględniając dzień i miesiąc urodzenia.
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
Trzeci przykład — praca z powiadomieniami. LocalDateTime jest używany do planowania przypomnień. Sprawdzamy, czy zaplanowany czas nadszedł.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
Wbudowane wsparcie java.time pojawiło się na Androidzie od API 26 (Android 8.0 Oreo). Dla urządzeń ze starszymi wersjami Androida należy użyć desugaring — mechanizmu, który dodaje wsparcie dla nowych API Java we wcześniejszych wersjach.
Desugaring w Android Gradle Plugin konfiguruje się przez compileOptions w build.gradle. Wystarczy ustawić isCoreLibraryDesugaringEnabled = true i dodać bibliotekę desugar_jdk_libs. Po tym java.time staje się dostępny dla wszystkich poziomów API od 14.
Dla projektów, które nie mogą użyć desugaring (np. legacy projekty na AGP poniżej 4.0), istnieje biblioteka ThreeTenABP — backport java.time. Udostępnia te same klasy (LocalDate, LocalTime, LocalDateTime), ale w pakiecie org.threeten.bp.
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
Pierwszy częsty błąd — używanie LocalDateTime w systemach rozproszonych bez uwzględnienia strefy czasowej. Jeśli serwer znajduje się w Europe/Moscow, a klient w Asia/Tokyo, LocalDateTime będzie interpretowany inaczej. Rozwiązanie: używać Instant lub ZonedDateTime dla danych globalnych.
Drugi błąd — nieprawidłowe parsowanie ciągów. Domyślnie LocalDate.parse() oczekuje formatu ISO-8601 (yyyy-MM-dd). Jeśli ciąg jest w innym formacie, należy jawnie przekazać DateTimeFormatter. Warto również obsługiwać DateTimeParseException, aby aplikacja nie padła przy nieprawidłowym wejściu.
Trzeci błąd — ignorowanie null-bezpieczeństwa. LocalDate, LocalTime i LocalDateTime to obiekty, które mogą być null. W Kotlin zaleca się używanie typów nullable z jawnym sprawdzeniem lub operatorem Elvisa. W Java — sprawdzać na null przed wywołaniem metod.
Czwarty błąd — pomylenie LocalDateTime z ZonedDateTime. LocalDateTime nie zawiera żadnej informacji o strefie czasowej. Jeśli trzeba przekazać absolutny moment w czasie — należy używać typów strefowych. Jeśli wystarczy czas lokalny — lokalnych.
Często zadawane pytania
Date przechowuje liczbę milisekund od 1970-01-01 UTC, a LocalDate przechowuje rok, miesiąc i dzień bez przywiązania do strefy czasowej. Date jest mutowalny i nie jest thread-safe, LocalDate — immutable i thread-safe. Date jest przestarzały od Java 8.
Tak, LocalDateTime dobrze mapuje się na typ SQL TIMESTAMP WITHOUT TIME ZONE. JPA i Room obsługują go przez TypeConverter. Dla TIMESTAMP WITH TIME ZONE należy użyć ZonedDateTime lub OffsetDateTime.
Użyj ChronoUnit.DAYS.between(startDate, endDate). Ta metoda zwraca long — różnicę w dniach. Do bardziej szczegółowych obliczeń użyj Period.between(), który zwraca Period z latami, miesiącami i dniami.
LocalTime obsługuje precyzję do nanosekund (9 miejsc po przecinku). Jeśli potrzebna jest precyzja do milisekund, użyj truncateTo(ChronoUnit.MILLIS) przed zapisaniem. Zapobiega to problemom z zaokrąglaniem przy serializacji.
Metoda now() używa zegara systemowego urządzenia i domyślnej strefy czasowej. Jeśli urządzenia znajdują się w różnych strefach czasowych, data może się różnić. Do jednolitego znacznika czasu używaj Instant.now(), który zawsze zwraca czas w UTC.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również