ZonedDateTime — какво е това, работа с часови зони и време

Автор: IT Sectr Публикувано: 2026-07-13 Време за четене: 10 мин

ZonedDateTime — immutable класа из пакета java.time која чува датум и време заједно са информацијама о временској зони (ZoneId). За разлику од LocalDateTime, ZonedDateTime једнозначно идентификује тренутак на временској скали. Према спецификацији Oracle Java 17 (2024), класа коректно обрађа прелазак на летње време (DST) кроз правила зоне из базе IANA Time Zone Database.

Главно

  • ZonedDateTime — immutable класа која обједињује датум, време и временску зону (ZoneId) у једном објекту.
  • За разлику од LocalDateTime, ZonedDateTime једнозначно одређује тренутак на временској скали и погодан је за глобалне системе.
  • Класа аутоматски обрађа прелазак на летње време (DST) у складу са правилима IANA Time Zone Database.
  • За конверзију између временских зона користи се метода withZoneSameInstant(ZoneId).
  • Чување ZonedDateTime у базама података препоручује се кроз OffsetDateTime или TIMESTAMP WITH TIME ZONE.

Шта је ZonedDateTime?

ZonedDateTime — једна од кључних класа пакета java.time, која представља датум и време са потпуним информацијама о временској зони. Она обједињује три компоненте: LocalDateTime (датум и време), ZoneId (идентификатор зоне) и ZoneOffset (помак у односу на UTC).

За разлику од LocalDateTime, који чува само зидно време (wall-clock time) без везе са зоном, ZonedDateTime једнозначно идентификује тренутак. Два иста LocalDateTime у различитим временским зонама представљају различите тренутке времена. Два иста ZonedDateTime — исти тренутак.

Класа је потпуно immutable и thread-safe. Све аритметичке операције враћају нови објекат. ZonedDateTime имплементира интерфејс ChronoZonedDateTime и може да се користи свуда где је потребан рад са зоналним временом у Java-и.

Према спецификацији Oracle Java 17, ZonedDateTime подржава рад са било којом зоном из IANA Time Zone Database, која укључује преко 600 временских зона.

ZonedDateTime vs LocalDateTime: у чему је разлика?

Главна разлика — ZonedDateTime садржи временску зону, а LocalDateTime — не. Ова фундаментална разлика одређује област примене сваке класе.

LocalDateTime се користи за локалне догађаје: време концерта, распоред часова, датум рођења. Ако се догађај одвија у Москви у 15:00, LocalDateTime ће забележити 15:00 без везе. Ако преместите сервер у Њујорк, време остаје 15:00 — али то је већ други физички тренутак.

ZonedDateTime се примењује за глобалне податке: серверске логове, временске ознаке у API-ју, међународни састанци. Ако је састанак заказан за 15:00 MSK, ZonedDateTime ће сачувати и време и зону. У Њујорку ће бити приказано као 8:00 EST. Према Baeldung (2024), избор између LocalDateTime и ZonedDateTime је најчешћа архитектурна одлука при раду са датумима.

Практично правило: ако се подаци чувају за један регион — користите LocalDateTime. Ако подаци прелазе границе временских зона — користите ZonedDateTime. Ако треба да пренесете апсолутни тренутак — користите Instant.

Како функционише временска зона у java.time?

Временску зону у java.time представља класа ZoneId. ZoneId је идентификатор зоне у формату „continent/region”, на пример „Europe/Moscow”, „America/New_York”, „Asia/Tokyo”. ZoneId се добија путем статичке методе of(String zoneId) или преко подразумеване системске временске зоне.

ZoneId се дели на два типа: fixed offset (фиксно померање, на пример „+03:00”) и region-based (регионалне зоне, на пример „Europe/London”). Регионалне зоне садрже правила преласка на летње време и историјске промјене. Fixed offset — само фиксно померање.

За добијање тренутног померања ZoneId у одређеном тренутку користи се метода getRules(), која враћа ZoneRules. ZoneRules садржи све преласке и померања за дату зону. Ово је кључни механизам за коректно обрађање DST-а.

Све временске зоне се испоручују са JDK кроз датотеке tzdata (IANA Time Zone Database) и редовно се ажурирају. На Android-у, верзија tzdata зависи од системских ажурирања путем Google Play Services.

Креирање ZonedDateTime

ZonedDateTime се може креирати на више начина. Најједноставнији је now(), који враћа тренутно време у подразумеваној системској временској зони. Варијанта now(ZoneId) омогућава добијање тренутног времена у наведеној зони.

Метода of(LocalDateTime, ZoneId) креира ZonedDateTime из локалног времена и зоне. Варијанта of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) — из компонената.

LocalDateTime се може претворити у ZonedDateTime путем методе atZone(ZoneId). Instant — путем Instant.atZone(ZoneId). Date — путем 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)

Конверзија између временских зона

Главна метода конверзије — withZoneSameInstant(ZoneId). Претвара ZonedDateTime у другу временску зону, чувајући исти тренутак. На пример, 15:00 MSK → 8:00 EST. Метода withZoneSameLocal(ZoneId) мења зону, чувајући локално време — то даје другачији тренутак.

За добијање померања у односу на UTC користи се метода getOffset(), која враћа ZoneOffset. ZoneOffset је наследник ZoneId који представља фиксно померање у формату „+HH:mm” или „-HH:mm”.

Претварање у Instant врши се путем методе toInstant(). Instant је апсолутни тренутак времена, независан од временске зоне. Обрнута конверзија — 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"))

Рад са летњим временом (DST)

Прелазак на летње време ствара два проблема: празнине (gap) и преклапања (overlap). Празнина настаје у пролеће, када се часовници померају напред — одређено време не постоји. Преклапање — у јесен, када се време помера уназад — исто време постоји два пута.

ZonedDateTime обрађа ове ситуације кроз стратегију resolve. При креирању објекта током празнине, java.time аутоматски помера време за износ померања. При креирању током преклапања, бира се прва варијанта (пре промене). Понашање се може променити путем withZoneSameInstant.

Може се проверити да ли је време у DST зони путем zone.getRules().isDaylightSavings(instant). Метода getOffset() приказује тренутно померање за дати тренутак, а getRules().getDaylightSavings(instant) — величину DST корекције у милисекундама.

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

Форматирање ZonedDateTime

За форматирање ZonedDateTime користи се DateTimeFormatter. Стандардни ISO формат укључује датум, време и померање: „2026-07-21T15:30:00+03:00[Europe/Moscow]”. Преддефинисани формати: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

За локализовано форматирање користите DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle може бити SHORT, MEDIUM, LONG, FULL. LONG укључује назив зоне („MSK”), FULL — пуни назив („Moscow Standard Time”).

Важно: при парсирању низке са ZonedDateTime, формат мора садржати информације о зони или померању. Ако зона није наведена, користите LocalDateTime.parse() и затим 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 у Android: практични примери

Први пример — приказ времена састанка кориснику у његовој временској зони. Сервер враћа ZonedDateTime у UTC-у, клијент конвертује у локалну временску зону уређаја.

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

Други пример — израчунавање времена до следећег догађаја узимајући у обзир временску зону. Користимо ZonedDateTime за серверско време и Duration.between() за израчунавање разлике.

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

Трећи пример — рад са Retrofit API-јем. Сервер враћа ниску у ISO-8601 са зоном. Користимо прилагођени десеријализатор за претварање у 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()
        )
    }
}

Грешке у раду са временским зонама

Прва грешка — коришћење ZoneId.systemDefault() у серверском коду. Временска зона сервера може се разликовати од клијентске, а коришћење системске зоне на серверу доводи до неисправних израчунавања. Увијек јасно наведите зону или користите UTC као референцу.

Друга грешка — игнорисање DST-а при израчунавању трајања. Duration.between() коректно обрађа преласке, али ако ручно одузимате timestamp-ове, прелазак на летње време може дати грешку од 1 сат. Користите методе ChronoUnit.HOURS.between() уместо ручне математике.

Трећа грешка — забуна између withZoneSameInstant и withZoneSameLocal. Прва мења зону, чувајући тренутак — време се помера. Друга мења зону, чувајући локално време — тренутак се мења. Избор погрешне методе је једна од најчешћих грешака према SonarSource (2024).

Четврта грешка — претпоставка да је временска зона уређаја увијек иста као и временска зона корисника. Корисник може да путује и очекује да апликација прикаже време у његовој „кућној” зони, а не у тренутној. У овом случају потребно је омогућити избор зоне кроз интерфејс.

Често постављана питања

Која је разлика између ZonedDateTime и OffsetDateTime?

ZonedDateTime садржи регионални идентификатор зоне (на пример „Europe/Moscow”) и обрађа DST. OffsetDateTime чува само фиксно померање (+03:00) без регионалних правила. За чување у бази података препоручује се OffsetDateTime.

Како добити тренутно време у UTC-у путем ZonedDateTime?

Користите ZonedDateTime.now(ZoneOffset.UTC) или Instant.now().atZone(ZoneOffset.UTC). Обе варијанте враћају тренутни тренутак са нултим померањем. За једноставну ознаку времена користите Instant.now() без везе за зону.

Може ли ZonedDateTime да се серијализује кроз Gson или Moshi?

Да, али је потребан прилагођени адаптер. Gson не подржава ZonedDateTime подразумевано. Moshi — подржава кроз Rfc3339DateJsonAdapter адаптер. Препоручује се коришћење Kotlinx Serialization или библиотеке JavaTimeModule за Jackson.

Како обрадити ситуацију када време пада у DST празнину?

java.time аутоматски помера време напред за износ померања. На пример, ако време 02:30 не постоји при преласку на 03:00, ZonedDateTime ће креирати објекат на 03:30. Присуство празнине може се проверити кроз ZoneRules.getTransition(instant).

Зашто ZonedDateTime није препоручлјив за SQL базе података?

JDBC 4.2 подржава OffsetDateTime, али не ZonedDateTime директно. ZonedDateTime садржи регионалну зону која нема аналогију у SQL-у. Препоручује се чување OffsetDateTime или Instant, а зона да се чува у засебној колони.

Резиме

  • ZonedDateTime — immutable класа за датум и време са временском зоном, која коректно обрађа DST кроз IANA Time Zone Database.
  • Основна разлика од LocalDateTime — присуство зоне, што чини ZonedDateTime једнозначним идентификатором тренутка времена.
  • За конверзију између зона користите withZoneSameInstant(), који чува тренутак, а не withZoneSameLocal.
  • При преласку на летње време, java.time аутоматски решава празнине и преклапања кроз уграђене правила зона.
  • За чување у бази података користите OffsetDateTime или чувајте Instant и ZoneId одвојено.
  • У Android-у, за претварање ZonedDateTime у локално време уређаја, користите ZoneId.systemDefault() са withZoneSameInstant.
  • За серијализацију у JSON потребан је прилагођени адаптер — користите Kotlinx Serialization или Jackson JavaTimeModule.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също