ZonedDateTime — o clasă immutable din pachetul java.time care stochează data și ora împreună cu informații despre fusul orar (ZoneId). Spre deosebire de LocalDateTime, ZonedDateTime identifică în mod unic momentul pe axa temporală. Conform specificației Oracle Java 17 (2024), clasa gestionează corect trecerea la ora de vară (DST) prin regulile zonei din baza de date IANA Time Zone Database.
Principalele
ZonedDateTime — una dintre clasele cheie ale pachetului java.time, reprezentând data și ora cu informații complete despre fusul orar. Acesta combină trei componente: LocalDateTime (data și ora), ZoneId (identificatorul zonei) și ZoneOffset (decalajul față de UTC).
Spre deosebire de LocalDateTime, care stochează doar timpul de perete (wall-clock time) fără legătură cu zona, ZonedDateTime identifică în mod unic momentul. Două LocalDateTime identice în fusuri orare diferite reprezintă momente de timp diferite. Două ZonedDateTime identice — același moment.
Clasa este complet immutable și thread-safe. Toate operațiile aritmetice returnează un obiect nou. ZonedDateTime implementează interfața ChronoZonedDateTime și poate fi utilizat oriunde este necesară lucrul cu timpul zonal în Java.
Conform specificației Oracle Java 17, ZonedDateTime suportă lucrul cu orice zonă din IANA Time Zone Database, care include peste 600 de fusuri orare.
Diferența principală — ZonedDateTime conține fusul orar, iar LocalDateTime — nu. Această diferență fundamentală determină domeniul de aplicare al fiecărei clase.
LocalDateTime este utilizat pentru evenimente locale: ora concertului, programul cursurilor, data nașterii. Dacă evenimentul are loc la Moscova la ora 15:00, LocalDateTime va înregistra 15:00 fără legătură. Dacă mutați serverul la New York, ora rămâne 15:00 — dar acesta este deja un alt moment fizic.
ZonedDateTime se aplică pentru date globale: loguri de server, marcaje de timp în API, întâlniri internaționale. Dacă întâlnirea este stabilită la ora 15:00 MSK, ZonedDateTime va păstra atât ora, cât și zona. În New York va fi afișat corect ca 8:00 EST. Potrivit Baeldung (2024), alegerea între LocalDateTime și ZonedDateTime este cea mai frecventă decizie arhitecturală la lucrul cu datele.
Regula practică: dacă datele sunt stocate pentru o singură regiune — utilizați LocalDateTime. Dacă datele traversează granițele fusurilor orare — utilizați ZonedDateTime. Dacă trebuie să transmiteți un moment absolut — utilizați Instant.
Fusul orar în java.time este reprezentat de clasa ZoneId. ZoneId este un identificator de zonă în formatul „continent/region”, de exemplu „Europe/Moscow”, „America/New_York”, „Asia/Tokyo”. ZoneId se obține prin metoda statică of(String zoneId) sau prin fusul orar implicit al sistemului.
ZoneId se împarte în două tipuri: fixed offset (decalaj fix, de exemplu „+03:00”) și region-based (zone regionale, de exemplu „Europe/London”). Zonele regionale conțin reguli de trecere la ora de vară și modificări istorice. Fixed offset — doar un decalaj fix.
Pentru a obține decalajul curent al ZoneId într-un moment specific, se utilizează metoda getRules(), care returnează ZoneRules. ZoneRules conține toate tranzițiile și decalajele pentru acea zonă. Acesta este mecanismul cheie pentru gestionarea corectă a DST.
Toate fusurile orare sunt furnizate cu JDK prin fișierele tzdata (IANA Time Zone Database) și sunt actualizate regulat. Pe Android, versiunea tzdata depinde de actualizările sistemului prin Google Play Services.
Există mai multe moduri de a crea ZonedDateTime. Cel mai simplu — now(), care returnează ora curentă în fusul orar al sistemului. Varianta now(ZoneId) permite obținerea orei curente într-o zonă specificată.
Metoda of(LocalDateTime, ZoneId) creează ZonedDateTime din timpul local și zonă. Varianta of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) — din componente.
LocalDateTime poate fi convertit în ZonedDateTime prin metoda atZone(ZoneId). Instant — prin Instant.atZone(ZoneId). Date — prin Date.toInstant().atZone(ZoneId).
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)
Metoda principală de conversie — withZoneSameInstant(ZoneId). Convertește ZonedDateTime într-un alt fus orar, păstrând același moment de timp. De exemplu, 15:00 MSK → 8:00 EST. Metoda withZoneSameLocal(ZoneId) schimbă zona, păstrând ora locală — aceasta dă un alt moment.
Pentru a obține decalajul față de UTC, se utilizează metoda getOffset(), care returnează ZoneOffset. ZoneOffset este un moștenitor al ZoneId care reprezintă un decalaj fix în formatul „+HH:mm” sau „-HH:mm”.
Conversia în Instant se realizează prin metoda toInstant(). Instant este un moment absolut de timp, independent de fusul orar. Conversia inversă — Instant.atZone(ZoneId).
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"))
Trecerea la ora de vară creează două probleme: goluri (gap) și suprapuneri (overlap). Golul apare primăvara, când ceasurile sunt date înainte — un anumit timp nu există. Suprapunerea — toamna, când timpul este dat înapoi — același timp există de două ori.
ZonedDateTime gestionează aceste situații prin strategia resolve. La crearea obiectului în timpul unui gol, java.time deplasează automat timpul cu valoarea decalajului. La crearea în timpul unei suprapuneri, se alege prima variantă (înainte de schimbare). Comportamentul poate fi modificat prin withZoneSameInstant.
Se poate verifica dacă timpul se află în zona DST prin zone.getRules().isDaylightSavings(instant). Metoda getOffset() arată decalajul curent pentru momentul respectiv, iar getRules().getDaylightSavings(instant) — valoarea corecției DST în milisecunde.
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", "Decalaj DST: $dstAmount")
}
}
Pentru formatarea ZonedDateTime se utilizează DateTimeFormatter. Formatul standard ISO include data, ora și decalajul: „2026-07-21T15:30:00+03:00[Europe/Moscow]”. Formate predefinite: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.
Pentru formatare localizată, utilizați DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle poate fi SHORT, MEDIUM, LONG, FULL. LONG include numele zonei („MSK”), FULL — numele complet („Moscow Standard Time”).
Important: la parsarea unui șir cu ZonedDateTime, formatul trebuie să conțină informații despre zonă sau decalaj. Dacă zona nu este specificată, utilizați LocalDateTime.parse() și apoi atZone().
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
)
Primul exemplu — afișarea orei întâlnirii pentru utilizator în propriul său fus orar. Serverul returnează ZonedDateTime în UTC, clientul convertește în fusul orar local al dispozitivului.
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)
}
Al doilea exemplu — calcularea timpului până la următorul eveniment, ținând cont de fusul orar. Folosim ZonedDateTime pentru timpul serverului și Duration.between() pentru calcularea diferenței.
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"
}
Al treilea exemplu — lucrul cu API Retrofit. Serverul returnează un șir în ISO-8601 cu zona. Folosim un dezserializator personalizat pentru conversia în ZonedDateTime.
data class EventResponse(
@JsonAdapter(ZonedDateTimeAdapter::class)
val eventTime: ZonedDateTime
)
class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
override fun fromJson(reader: JsonReader): ZonedDateTime? {
return ZonedDateTime.parse(
reader.nextString()
)
}
}
Prima eroare — utilizarea ZoneId.systemDefault() în codul serverului. Fusul orar al serverului poate diferi de cel al clientului, iar utilizarea fusului de sistem pe server duce la calcule incorecte. Specificați întotdeauna zona în mod explicit sau utilizați UTC ca referință.
A doua eroare — ignorarea DST la calcularea duratei. Duration.between() gestionează corect tranzițiile, dar dacă scădeți manual timestamp-urile, trecerea la ora de vară poate da o eroare de 1 oră. Utilizați metodele ChronoUnit.HOURS.between() în loc de calcule manuale.
A treia eroare — confuzia între withZoneSameInstant și withZoneSameLocal. Primul schimbă zona, păstrând momentul — ora se deplasează. Al doilea schimbă zona, păstrând ora locală — momentul se schimbă. Alegerea metodei greșite este una dintre cele mai frecvente erori conform SonarSource (2024).
A patra eroare — presupunerea că fusul orar al dispozitivului este întotdeauna același cu fusul orar al utilizatorului. Utilizatorul poate călători și poate aștepta ca aplicația să afișeze ora în fusul său „de acasă”, nu în cel curent. În acest caz, trebuie să oferiți alegerea zonei prin interfață.
Întrebări frecvente
ZonedDateTime conține un identificator regional de zonă (de exemplu „Europe/Moscow”) și gestionează DST. OffsetDateTime stochează doar un decalaj fix (+03:00) fără reguli regionale. Pentru stocarea în baza de date se recomandă OffsetDateTime.
Utilizați ZonedDateTime.now(ZoneOffset.UTC) sau Instant.now().atZone(ZoneOffset.UTC). Ambele variante returnează momentul curent cu decalaj zero. Pentru un marcaj de timp simplu, utilizați Instant.now() fără legătură cu zona.
Da, dar este necesar un adapter personalizat. Gson nu suportă ZonedDateTime implicit. Moshi — suportă prin adapterul Rfc3339DateJsonAdapter. Se recomandă utilizarea Kotlinx Serialization sau a bibliotecii JavaTimeModule pentru Jackson.
java.time deplasează automat timpul înainte cu valoarea decalajului. De exemplu, dacă ora 02:30 nu există la trecerea la 03:00, ZonedDateTime va crea un obiect la 03:30. Prezența golului poate fi verificată prin ZoneRules.getTransition(instant).
JDBC 4.2 suportă OffsetDateTime, dar nu ZonedDateTime direct. ZonedDateTime conține o zonă regională care nu are analog în SQL. Se recomandă stocarea OffsetDateTime sau Instant, iar zona să fie stocată într-o coloană separată.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și