LocalDate, LocalTime i LocalDateTime: co to jest, praca z datą

Autor: IT Sectr Opublikowano: 2026-07-13 Czas czytania: 12 min

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 — immutable klasa do reprezentacji daty (rok, miesiąc, dzień) bez czasu i strefy czasowej.
  • LocalTime — immutable klasa do reprezentacjii czasu (godzina, minuta, sekunda, nanosekunda) bez daty i strefy czasowej.
  • LocalDateTime — kombinacja LocalDate i LocalTime, przechowująca datę i czas bez przywiązania do strefy.
  • Wszystkie trzy klasy obsługują operacje arytmetyczne — dodawanie i odejmowanie dni, miesięcy, godzin za pomocą metod plus i minus.
  • Na Androidzie te typy są dostępne poprzez desugaring (API 26+) lub bibliotekę ThreeTenABP (API < 26).

Czym są LocalDate, LocalTime i LocalDateTime?

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.

Jak zbudowany jest pakiet java.time?

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ę.

LocalDate: praca z datą

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: praca z czasem

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: kombinacja daty i czasu

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.

Jak tworzyć obiekty daty i czasu?

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:

  • now() — bieżąca data/czas z zegara systemowego
  • of(...) — z komponentów (rok, miesiąc, dzień itd.)
  • parse(String) — z ciągu w formacie ISO-8601
  • from(TemporalAccessor) — z innego obiektu temporalnego

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.

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)

Konwersja między typami

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).

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"))

Formatowanie i parsowanie

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.

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")
)

Porównywanie obiektów daty-czasu

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.

kotlin
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()

Arytmetyka daty i czasu

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.

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)

Przykłady kodu w Kotlin

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.

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()
    }
}

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.

kotlin
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ł.

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

Wsparcie w Android: poziom API i desugaring

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.

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

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

Typowe błędy i jak ich unikać

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

Jaka jest różnica między LocalDate a Date w Java?

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.

Czy można używać LocalDateTime w bazie danych?

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.

Jak uzyskać liczbę dni między dwiema datami?

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.

Co zrobić, jeśli potrzebuję zachować precyzję czasu do milisekund?

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.

Dlaczego LocalDate.now() zwraca inną datę na różnych urządzeniach?

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

  • LocalDate — immutable klasa dla daty bez czasu i strefy czasowej. Używana do przechowywania dat urodzenia, terminów, dat wydarzeń.
  • LocalTime — immutable klasa dla pory dnia z precyzją do nanosekundy. Nadaje się do przechowywania godzin otwarcia, czasu trwania procesów.
  • LocalDateTime — kombinacja daty i czasu bez przywiązania do strefy. Najbardziej elastyczny typ lokalny, ale nie nadaje się do systemów rozproszonych.
  • Wszystkie trzy klasy obsługują arytmetykę, porównywanie, formatowanie i parsowanie przez jednolity API oparty na DateTimeFormatter.
  • Na Androidzie java.time jest dostępny przez wbudowane wsparcie od API 26 lub przez desugaring dla starszych wersji.
  • Do globalnych znaczników czasu i danych ze strefami czasowymi używaj ZonedDateTime lub Instant zamiast typów lokalnych.
  • Podczas parsowania ciągów zawsze przekazuj DateTimeFormatter dla niestandardowych formatów i obsługuj DateTimeParseException.

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.

Omów projekt

Przeczytaj również