ZonedDateTime — co to je, práce s časovými pásmy a časem

Autor: IT Sectr Publikováno: 2026-07-13 Doba čtení: 10 min

ZonedDateTime — immutable klasa z pakietu java.time, która przechowuje datę i czas wraz z informacją o strefie czasowej (ZoneId). W przeciwieństwie do LocalDateTime, ZonedDateTime jednoznacznie identyfikuje moment na osi czasu. Według specyfikacji Oracle Java 17 (2024), klasa poprawnie obsługuje przejście na czas letni (DST) poprzez reguły strefy z bazy danych IANA Time Zone Database.

Główne

  • ZonedDateTime — immutable klasa, łącząca datę, czas i strefę czasową (ZoneId) w jednym obiekcie.
  • W przeciwieństwie do LocalDateTime, ZonedDateTime jednoznacznie określa moment na osi czasu i nadaje się do systemów globalnych.
  • Klasa automatycznie obsługuje przejście na czas letni (DST) zgodnie z regułami IANA Time Zone Database.
  • Do konwersji między strefami czasowymi używa się metody withZoneSameInstant(ZoneId).
  • Przechowywanie ZonedDateTime w bazach danych zaleca się poprzez OffsetDateTime lub TIMESTAMP WITH TIME ZONE.

Co to jest ZonedDateTime?

ZonedDateTime — jeden z kluczowych klas pakietu java.time, reprezentujący datę i czas z pełną informacją o strefie czasowej. Łączy trzy komponenty: LocalDateTime (data i czas), ZoneId (identyfikator strefy) i ZoneOffset (przesunięcie względem UTC).

W przeciwieństwie do LocalDateTime, który przechowuje tylko czas ścienny (wall-clock time) bez przypisania do strefy, ZonedDateTime jednoznacznie identyfikuje moment. Dwa identyczne LocalDateTime w różnych strefach czasowych reprezentują różne momenty czasu. Dwa identyczne ZonedDateTime — ten sam moment.

Klasa jest w pełni immutable i thread-safe. Wszystkie operacje arytmetyczne zwracają nowy obiekt. ZonedDateTime implementuje interfejs ChronoZonedDateTime i może być używany wszędzie, gdzie wymagana jest praca z czasem strefowym w Javie.

Według specyfikacji Oracle Java 17, ZonedDateTime obsługuje pracę z dowolną strefą z IANA Time Zone Database, która obejmuje ponad 600 stref czasowych.

ZonedDateTime vs LocalDateTime: jaka jest różnica?

Główna różnica — ZonedDateTime zawiera strefę czasową, a LocalDateTime — nie. To fundamentalne odróżnienie określa zakres zastosowania każdej klasy.

LocalDateTime jest używany do lokalnych wydarzeń: czas koncertu, harmonogram zajęć, data urodzenia. Jeśli wydarzenie ma miejsce w Moskwie o 15:00, LocalDateTime zarejestruje 15:00 bez przypisania. Jeśli przeniesiesz serwer do Nowego Jorku, czas pozostanie 15:00 — ale będzie to już inny fizyczny moment.

ZonedDateTime stosuje się do danych globalnych: logi serwera, znaczniki czasu w API, międzynarodowe spotkania. Jeśli spotkanie jest umówione na 15:00 MSK, ZonedDateTime zachowa zarówno czas, jak i strefę. W Nowym Jorku będzie poprawnie wyświetlane jako 8:00 EST. Według Baeldung (2024), wybór między LocalDateTime a ZonedDateTime to najczęstsza decyzja architektoniczna przy pracy z datami.

Praktyczna zasada: jeśli dane są przechowywane dla jednego regionu — używaj LocalDateTime. Jeśli dane przekraczają granice stref czasowych — używaj ZonedDateTime. Jeśli potrzebujesz przekazać absolutny moment — używaj Instant.

Jak działa strefa czasowa w java.time?

Strefa czasowa w java.time jest reprezentowana przez klasę ZoneId. ZoneId to identyfikator strefy w formacie „continent/region”, na przykład „Europe/Moscow”, „America/New_York”, „Asia/Tokyo”. ZoneId uzyskuje się poprzez statyczną metodę of(String zoneId) lub przez systemową strefę czasową domyślną.

ZoneId dzieli się na dwa typy: fixed offset (stałe przesunięcie, na przykład „+03:00”) i region-based (strefy regionalne, na przykład „Europe/London”). Strefy regionalne zawierają reguły przejścia na czas letni i historyczne zmiany. Fixed offset to tylko stałe przesunięcie.

Aby uzyskać aktualne przesunięcie ZoneId w konkretnym momencie, używa się metody getRules(), która zwraca ZoneRules. ZoneRules zawiera wszystkie przejścia i przesunięcia dla danej strefy. To kluczowy mechanizm do poprawnej obsługi DST.

Wszystkie strefy czasowe są dostarczane z JDK poprzez pliki tzdata (IANA Time Zone Database) i regularnie aktualizowane. Na Android wersja tzdata zależy od aktualizacji systemu poprzez Google Play Services.

Tworzenie ZonedDateTime

Można utworzyć ZonedDateTime na kilka sposobów. Najprostszy — now(), który zwraca aktualny czas w systemowej strefie czasowej. Wariant now(ZoneId) umożliwia uzyskanie aktualnego czasu w określonej strefie.

Metoda of(LocalDateTime, ZoneId) tworzy ZonedDateTime z czasu lokalnego i strefy. Wariant of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) — z komponentów.

LocalDateTime można przekonwertować na ZonedDateTime poprzez metodę atZone(ZoneId). Instant — poprzez Instant.atZone(ZoneId). Date — poprzez Date.toInstant().atZone(ZoneId).

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

Konwersja między strefami czasowymi

Główna metoda konwersji — withZoneSameInstant(ZoneId). Przekształca ZonedDateTime w inną strefę czasową, zachowując ten sam moment czasu. Na przykład 15:00 MSK → 8:00 EST. Metoda withZoneSameLocal(ZoneId) zmienia strefę, zachowując czas lokalny — daje to inny moment.

Aby uzyskać przesunięcie względem UTC, używa się metody getOffset(), zwracającej ZoneOffset. ZoneOffset to dziedzic ZoneId, który reprezentuje stałe przesunięcie w formacie „+HH:mm” lub „-HH:mm”.

Konwersja na Instant jest wykonywana poprzez metodę toInstant(). Instant to absolutny moment czasu, niezależny od strefy czasowej. Odwrotna konwersja — Instant.atZone(ZoneId).

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

Praca z czasem letnim (DST)

Przejście na czas letni tworzy dwa problemy: luki (gap) i nakładania (overlap). Luka powstaje wiosną, gdy zegary są przesuwane do przodu — określony czas nie istnieje. Nakładanie — jesienią, gdy czas jest cofany — ten sam czas występuje dwukrotnie.

ZonedDateTime obsługuje te sytuacje poprzez strategię resolve. Podczas tworzenia obiektu w czasie luki java.time automatycznie przesuwa czas o wielkość przesunięcia. Podczas tworzenia w czasie nakładania wybierany jest pierwszy wariant (przed zmianą). Zachowanie można zmienić poprzez withZoneSameInstant.

Sprawdzić, czy czas znajduje się w strefie DST, można poprzez zone.getRules().isDaylightSavings(instant). Metoda getOffset() pokazuje aktualne przesunięcie dla danego momentu, a getRules().getDaylightSavings(instant) — wielkość korekty DST w milisekundach.

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("DST", "DST offset: ")
    }
}

Formatowanie ZonedDateTime

Do formatowania ZonedDateTime używa się DateTimeFormatter. Standardowy format ISO obejmuje datę, czas i przesunięcie: „2026-07-21T15:30:00+03:00[Europe/Moscow]”. Predefiniowane formaty: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Do zlokalizowanego formatowania użyj DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle może być SHORT, MEDIUM, LONG, FULL. LONG zawiera nazwę strefy („MSK”), FULL — pełną nazwę („Moscow Standard Time”).

Ważne: podczas parsowania ciągu z ZonedDateTime format musi zawierać informację o strefie lub przesunięciu. Jeśli strefa nie jest określona, użyj LocalDateTime.parse(), a następnie atZone().

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

ZonedDateTime w Android: praktyczne przykłady

Pierwszy przykład — wyświetlanie czasu spotkania dla użytkownika w jego strefie czasowej. Serwer zwraca ZonedDateTime w UTC, klient konwertuje na lokalną strefę czasową urządzenia.

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

Drugi przykład — obliczanie czasu do następnego wydarzenia z uwzględnieniem strefy czasowej. Używamy ZonedDateTime dla czasu serwerowego i Duration.between() do obliczenia różnicy.

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

Trzeci przykład — praca z API Retrofit. Serwer zwraca ciąg w ISO-8601 ze strefą. Używamy niestandardowego deserializatora do konwersji na ZonedDateTime.

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

Błędy przy pracy ze strefami czasowymi

Pierwszy błąd — używanie ZoneId.systemDefault() w kodzie serwerowym. Strefa czasowa serwera może się różnić od klienckiej, a używanie systemowej strefy na serwerze prowadzi do nieprawidłowych obliczeń. Zawsze jawnie określaj strefę lub używaj UTC jako punktu odniesienia.

Drugi błąd — ignorowanie DST przy obliczaniu czasu trwania. Duration.between() poprawnie obsługuje przejścia, ale jeśli ręcznie odejmujesz timestampy, przejście na czas letni może dać błąd o 1 godzinę. Używaj metod ChronoUnit.HOURS.between() zamiast ręcznej matematyki.

Trzeci błąd — mylenie withZoneSameInstant z withZoneSameLocal. Pierwszy zmienia strefę, zachowując moment — czas się przesuwa. Drugi zmienia strefę, zachowując czas lokalny — moment się zmienia. Wybór nieprawidłowej metody to jeden z najczęstszych błędów według SonarSource (2024).

Czwarty błąd — założenie, że strefa czasowa urządzenia to zawsze to samo co strefa czasowa użytkownika. Użytkownik może podróżować i oczekiwać, że aplikacja pokaże czas w jego „domowej” strefie, a nie w bieżącej. W takim przypadku należy umożliwić wybór strefy poprzez interfejs.

Często zadawane pytania

Jaka jest różnica między ZonedDateTime a OffsetDateTime?

ZonedDateTime zawiera regionalny identyfikator strefy (na przykład „Europe/Moscow”) i obsługuje DST. OffsetDateTime przechowuje tylko stałe przesunięcie (+03:00) bez reguł regionalnych. Do przechowywania w bazie danych zaleca się OffsetDateTime.

Jak uzyskać bieżący czas w UTC przez ZonedDateTime?

Użyj ZonedDateTime.now(ZoneOffset.UTC) lub Instant.now().atZone(ZoneOffset.UTC). Oba warianty zwracają bieżący moment z zerowym przesunięciem. Dla prostego znacznika czasu użyj Instant.now() bez przypisania do strefy.

Czy można serializować ZonedDateTime przez Gson lub Moshi?

Tak, ale wymagany jest niestandardowy adapter. Gson nie obsługuje ZonedDateTime domyślnie. Moshi — obsługuje przez adapter Rfc3339DateJsonAdapter. Zaleca się używanie Kotlinx Serialization lub biblioteki JavaTimeModule dla Jackson.

Jak obsłużyć sytuację, gdy czas trafia na lukę DST?

java.time automatycznie przesuwa czas do przodu o wielkość przesunięcia. Na przykład, jeśli czas 02:30 nie istnieje przy przejściu na 03:00, ZonedDateTime utworzy obiekt na 03:30. Sprawdzić obecność luki można przez ZoneRules.getTransition(instant).

Dlaczego ZonedDateTime nie jest zalecany dla SQL baz danych?

JDBC 4.2 obsługuje OffsetDateTime, ale nie ZonedDateTime bezpośrednio. ZonedDateTime zawiera regionalną strefę, która nie ma odpowiednika w SQL. Zaleca się przechowywanie OffsetDateTime lub Instant, a strefę przechowywać w osobnej kolumnie.

Podsumowanie

  • ZonedDateTime — immutable klasa dla daty i czasu ze strefą czasową, poprawnie obsługująca DST poprzez IANA Time Zone Database.
  • Główna różnica od LocalDateTime — obecność strefy, co czyni ZonedDateTime jednoznacznym identyfikatorem momentu czasu.
  • Do konwersji między strefami używaj withZoneSameInstant(), który zachowuje moment, a nie withZoneSameLocal.
  • Przy przejściu na czas letni java.time automatycznie rozwiązuje luki i nakładania poprzez wbudowane reguły stref.
  • Do przechowywania w bazie danych używaj OffsetDateTime lub przechowuj Instant i ZoneId oddzielnie.
  • W Android do konwersji ZonedDateTime na lokalny czas urządzenia używaj ZoneId.systemDefault() w parze z withZoneSameInstant.
  • Przy serializacji do JSON wymagany jest niestandardowy adapter — użyj Kotlinx Serialization lub Jackson JavaTimeModule.

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také