Java ZonedDateTime
Rappresenta date e orari con fuso orario in Java usando ZonedDateTime e la classe ZoneId.
ZonedDateTime è un LocalDateTime con un ZoneId allegato. Dice: "questa data e quest'ora del calendario, in questo luogo." La combinazione identifica un singolo momento nella timeline globale — 2025-11-04T14:00 [America/New_York] è esattamente un Instant, distinto da 2025-11-04T14:00 [Europe/Berlin].
Questa è la classe a cui si ricorre ogni volta che conta l'ora locale di un evento in un posto specifico. Calendari di riunioni. Job pianificati simili a cron che devono scattare alle "9:00 nel fuso dell'utente." Qualsiasi cosa che debba sopravvivere a una transizione DST. LocalDateTime non sa abbastanza; Instant è in UTC e non porta l'etichetta di fuso significativa per gli esseri umani. ZonedDateTime è entrambe le cose.
ZoneId: il catalogo dei fusi orari
Prima di ZonedDateTime, vediamo il ZoneId stesso — un fuso orario è identificato da un ZoneId, che si ottiene con ZoneId.of(...):
ZoneId ny = ZoneId.of("America/New_York");
ZoneId de = ZoneId.of("Europe/Berlin");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZoneId utc = ZoneId.of("UTC");
ZoneId sys = ZoneId.systemDefault();Le stringhe sono identificatori del IANA Time Zone Database (Regione/Città). L'elenco completo è ZoneId.getAvailableZoneIds() — circa 600 voci, aggiornate periodicamente quando i paesi cambiano il loro fuso o le regole DST. ZoneId porta lo storico dell'IANA, quindi le date nel 1985 utilizzano le regole che erano in vigore nel 1985.
Evita ZoneOffset (un fisso ±HH:MM) quando intendi un fuso reale. ZoneOffset.of("-05:00") è corretto per New York a novembre e sbagliato a giugno; ZoneId.of("America/New_York") è corretto tutto l'anno.
I nomi di fuso a tre lettere come "EST" e "PST" sono per lo più alias ormai, ambigui (era Eastern Standard o Eastern Australia?) e silenziosamente deprecati. Usa Regione/Città. "UTC" e "GMT" sono casi speciali e vanno bene.
Creazione
ZonedDateTime now = ZonedDateTime.now(); // system zone
ZonedDateTime nowNY = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime made = ZonedDateTime.of(2025, 11, 4, 14, 0, 0, 0, ZoneId.of("America/New_York"));
ZonedDateTime parsed = ZonedDateTime.parse("2025-11-04T14:00:00-05:00[America/New_York]");Il percorso di costruzione più comune è "ho un LocalDateTime, ho un ZoneId, collegali":
LocalDateTime ldt = LocalDateTime.of(2025, 11, 4, 14, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));atZone(zone) è il ponte con una sola chiamata da una lettura dell'orologio locale a un momento con fuso. Gestisce anche i due casi limite introdotti dal DST.
DST: quando l'orologio salta o si ripete
Due volte l'anno, l'orologio a muro in qualsiasi fuso che osserva il DST salta o si ripete. Quando avanza — negli Stati Uniti, le 02:00 saltano alle 03:00 in una domenica di marzo — le ore tra le 02:00 e le 03:00 non esistono in quel giorno. Quando torna indietro, le ore tra le 01:00 e le 02:00 accadono due volte. ZonedDateTime deve fare qualcosa in entrambi i casi, e ciò che fa è documentato:
- Ora saltata (gap):
atZonerestituisce l'ora post-transizione.LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York"))diventa03:30-04:00— il JDK ha avanzato di un'ora per atterrare su un orario valido. - Ora ripetuta (overlap):
atZonerestituisce il primo dei due momenti validi (quello prima del cambio di offset). UsawithEarlierOffsetAtOverlap()owithLaterOffsetAtOverlap()per scegliere esplicitamente.
ZonedDateTime ambiguous = LocalDateTime.of(2025, 11, 2, 1, 30)
.atZone(ZoneId.of("America/New_York")); // 01:30 EDT (earlier)
ZonedDateTime explicit = ambiguous.withLaterOffsetAtOverlap(); // 01:30 EST (later)I due ZonedDateTime hanno lo stesso LocalDateTime ma offset diversi e Instant diversi. Questo è l'unico posto in java.time in cui la stessa lettura dell'orologio locale si mappa legittimamente su due momenti — ed è la fonte dei bug legati al DST di cui hai sentito parlare. Sii deliberato quando l'overlap ha importanza.
Decomposizione
ZoneId zone = zdt.getZone();
ZoneOffset offset = zdt.getOffset();
LocalDateTime ldt = zdt.toLocalDateTime();
LocalDate date = zdt.toLocalDate();
LocalTime time = zdt.toLocalTime();
Instant inst = zdt.toInstant();
OffsetDateTime odt = zdt.toOffsetDateTime();Gli accessori si dividono in tre gruppi: la metà del fuso (getZone, getOffset), la metà dell'orologio locale (toLocalDateTime, toLocalDate, toLocalTime), e la metà del momento globale (toInstant). Tutte e tre sono simultaneamente vere per lo stesso ZonedDateTime; scegli la proiezione di cui hai bisogno.
OffsetDateTime è un tipo correlato — LocalDateTime più un ZoneOffset (nessun fuso, nessun DST). È utile per serializzare "2025-11-04T14:00-05:00" senza impegnarsi in un fuso con nome (spesso è ciò che vogliono i timestamp JSON); per qualsiasi codice che necessita di aritmetica DST-aware, mantieni il ZonedDateTime.
Due varianti di "giorno successivo"
ZonedDateTime ha due metodi che sembrano simili ma non lo sono:
zdt.plusDays(1); // add 1 day to the local clock reading
zdt.plus(Duration.ofHours(24)); // add exactly 24 hoursIn un giorno di transizione DST, i due divergono. Nel giorno in cui gli orologi avanzano, plusDays(1) atterra alla stessa ora locale di domani (che è solo 23 ore di tempo reale più avanti). plus(Duration.ofHours(24)) atterra a un'ora dell'orologio a muro un'ora dopo rispetto a ieri.
| Obiettivo | Metodo |
|---|---|
| "Stessa ora domani" (calendario) | plusDays(1) |
| "Esattamente 24 ore da ora" (durata) | plus(Duration.ofHours(24)) |
Entrambi sono corretti; rispondono a domande diverse. Scegli consapevolmente.
Confronti e uguaglianza
zdt1.isBefore(zdt2); // compares Instants
zdt1.isAfter(zdt2);
zdt1.isEqual(zdt2); // compares Instants
zdt1.equals(zdt2); // compares LocalDateTime + Zone + OffsetLa distinzione è netta:
isBefore/isAfter/isEqualconfrontano i momenti sottostanti (Instant).equalsconfronta la struttura completa — dueZonedDateTimeche rappresentano lo stesso momento ma hanno fusi diversi non sonoequal.
Per "sono questi lo stesso momento indipendentemente dal fuso," usa isEqual o converti entrambi in Instant e confronta.
Un esempio pratico: una riunione tra tre uffici
Il programma seguente pianifica una riunione per le 14:00 ora di Berlino e calcola a che ora corrisponde negli uffici di New York e Tokyo. Poi pianifica una riunione settimanale ricorrente che sopravvive a una transizione DST, dimostrando la differenza tra plusDays(7) e plus(Duration.ofDays(7)) in una settimana di transizione.
Cosa trarre dall'esecuzione:
withZoneSameInstant(otherZone)è l'operazione per "che ora è nel loro ufficio?" — mantiene il momento fisso e visualizza nuovamente l'orologio a muro nel nuovo fuso. Il suo gemellowithZoneSameLocal(otherZone)mantiene l'orologio a muro e cambia il momento (la riunione si sposta). I nomi si confondono facilmente; la differenza è quale cosa rimane la stessa. Leggili attentamente quando li scrivi.berlin.equals(ny)erafalseanche se i due rappresentavano lo stesso momento.equalsconfronta la struttura completa (data-ora locale + fuso). Per "stesso momento indipendentemente da come è etichettato," usaisEqualo confronta gliInstant. Questa è esattamente la stessa distinzione cheLocalDate.equalsvsisEqualfaceva —equalsper "stesso oggetto-valore,"isEqualper "stesso punto nel tempo."- Il gap DST (
2025-03-09 02:30in NY) è stato risolto avanzando a03:30-04:00. Il JDK non ha lanciato eccezioni; ha scelto il momento post-transizione. Se hai assolutamente bisogno di rilevare che hai fornito un orario impossibile, usaZoneRules.getTransition(localDateTime)e controlla se l'oggetto restituito è un gap. - L'overlap DST (
2025-11-02 01:30in NY) ti ha dato dueZonedDateTimedistinti con gli stessi campi locali e offset diversi —EDTvsEST, distanti un'ora.withLaterOffsetAtOverlap()ewithEarlierOffsetAtOverlap()sono il modo per scegliere. Se stai memorizzando eventi pianificati, decidi in anticipo quale dei due intende l'utente e applica la chiamata giusta al momento dell'analisi. plusDays(1)eplus(Duration.ofHours(24))hanno prodotto risultati diversi nel giorno dell'avanzamento dell'ora — 23 ore di tempo reale vs 24 ore, atterrando su orari dell'orologio diversi. UsaplusDays/plusWeeksper la pianificazione a forma di calendario ("stessa ora domani") eplus(Duration)per l'aritmetica del tempo trascorso ("allarme tra 24 ore"). La scelta segue quasi sempre l'intento dell'utente.
Cosa c'è dopo
ZonedDateTime è il lato human-friendly di "un momento con un'etichetta." Il capitolo successivo, Java Instant, è il lato machine-friendly — un momento come nanosecondi dall'epoch, nessun fuso, nessun calendario, il tipo che ogni sistema distribuito usa in rete.