W3docs

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): atZone restituisce l'ora post-transizione. LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York")) diventa 03:30-04:00 — il JDK ha avanzato di un'ora per atterrare su un orario valido.
  • Ora ripetuta (overlap): atZone restituisce il primo dei due momenti validi (quello prima del cambio di offset). Usa withEarlierOffsetAtOverlap() o withLaterOffsetAtOverlap() 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 hours

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

ObiettivoMetodo
"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 + Offset

La distinzione è netta:

  • isBefore/isAfter/isEqual confrontano i momenti sottostanti (Instant).
  • equals confronta la struttura completa — due ZonedDateTime che rappresentano lo stesso momento ma hanno fusi diversi non sono equal.

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.

java— editable, runs on the server

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 gemello withZoneSameLocal(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) era false anche se i due rappresentavano lo stesso momento. equals confronta la struttura completa (data-ora locale + fuso). Per "stesso momento indipendentemente da come è etichettato," usa isEqual o confronta gli Instant. Questa è esattamente la stessa distinzione che LocalDate.equals vs isEqual faceva — equals per "stesso oggetto-valore," isEqual per "stesso punto nel tempo."
  • Il gap DST (2025-03-09 02:30 in NY) è stato risolto avanzando a 03: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, usa ZoneRules.getTransition(localDateTime) e controlla se l'oggetto restituito è un gap.
  • L'overlap DST (2025-11-02 01:30 in NY) ti ha dato due ZonedDateTime distinti con gli stessi campi locali e offset diversi — EDT vs EST, distanti un'ora. withLaterOffsetAtOverlap() e withEarlierOffsetAtOverlap() 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) e plus(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. Usa plusDays/plusWeeks per la pianificazione a forma di calendario ("stessa ora domani") e plus(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.

Esercitazione

Pratica
Pianifichi una riunione ricorrente alle 09:00 America/New_York e memorizzi la prossima occorrenza con `nextMeeting.plusDays(7)`. Nella settimana che attraversa la transizione DST di primavera (spring-forward), cosa è vero del risultato?
Pianifichi una riunione ricorrente alle 09:00 America/New_York e memorizzi la prossima occorrenza con `nextMeeting.plusDays(7)`. Nella settimana che attraversa la transizione DST di primavera (spring-forward), cosa è vero del risultato?
Was this page helpful?