ZonedDateTime — cos’è, lavorare con i fusi orari e le date

Autore: IT Sectr Pubblicato: 2026-07-13 Tempo di lettura: 10 min

ZonedDateTime è una classe immutabile del pacchetto java.time che memorizza data e ora insieme alle informazioni sul fuso orario (ZoneId). A differenza di LocalDateTime, ZonedDateTime identifica in modo univoco un momento sulla linea temporale. Secondo le specifiche di Oracle Java 17 (2024), la classe gestisce correttamente le transizioni dell’ora legale (DST) attraverso le regole del fuso orario del database IANA Time Zone Database.

Punti Chiave

  • ZonedDateTime è una classe immutabile che combina data, ora e fuso orario (ZoneId) in un unico oggetto.
  • A differenza di LocalDateTime, ZonedDateTime definisce in modo univoco un momento sulla linea temporale ed è adatto per sistemi globali.
  • La classe gestisce automaticamente le transizioni dell’ora legale (DST) secondo le regole del database IANA Time Zone Database.
  • Per convertire tra fusi orari, utilizzare il metodo withZoneSameInstant(ZoneId).
  • Si consiglia di memorizzare ZonedDateTime nei database tramite OffsetDateTime o TIMESTAMP WITH TIME ZONE.

Cos’è ZonedDateTime?

ZonedDateTime è una delle classi chiave del pacchetto java.time, che rappresenta data e ora con informazioni complete sul fuso orario. Combina tre componenti: LocalDateTime (data e ora), ZoneId (identificatore del fuso) e ZoneOffset (offset rispetto a UTC).

A differenza di LocalDateTime, che memorizza solo l’ora locale (wall-clock time) senza legame con il fuso, ZonedDateTime identifica in modo univoco un momento. Due istanze identiche di LocalDateTime in fusi orari diversi rappresentano momenti diversi. Due istanze identiche di ZonedDateTime rappresentano lo stesso momento.

La classe è completamente immutabile e thread-safe. Tutte le operazioni aritmetiche restituiscono un nuovo oggetto. ZonedDateTime implementa l’interfaccia ChronoZonedDateTime e può essere utilizzata ovunque sia necessaria la gestione del tempo zonale in Java.

Secondo le specifiche di Oracle Java 17, ZonedDateTime supporta il lavoro con qualsiasi fuso del database IANA Time Zone Database, che include oltre 600 fusi orari.

ZonedDateTime vs LocalDateTime: qual è la differenza?

La differenza principale — ZonedDateTime contiene un fuso orario, mentre LocalDateTime no. Questa differenza fondamentale determina l’ambito di applicazione di ciascuna classe.

LocalDateTime viene utilizzato per eventi locali: ora del concerto, orario delle lezioni, data di nascita. Se un evento si verifica a Mosca alle 15:00, LocalDateTime registrerà le 15:00 senza alcun legame. Se si sposta il server a New York, l’ora rimarrà 15:00 — ma sarà un momento fisico diverso.

ZonedDateTime viene utilizzato per dati globali: log del server, timestamp API, riunioni internazionali. Se una riunione è fissata per le 15:00 MSK, ZonedDateTime conserverà sia l’ora che il fuso. A New York, verrà visualizzato correttamente come 8:00 EST. Secondo Baeldung (2024), la scelta tra LocalDateTime e ZonedDateTime è la decisione architettonica più comune quando si lavora con le date.

Regola pratica: se i dati sono memorizzati per una singola regione — utilizzare LocalDateTime. Se i dati attraversano i confini dei fusi orari — utilizzare ZonedDateTime. Se è necessario rappresentare un momento assoluto — utilizzare Instant.

Come funziona il fuso orario in java.time?

Il fuso orario in java.time è rappresentato dalla classe ZoneId. ZoneId è un identificatore di fuso nel formato “continente/regione”, ad esempio “Europe/Moscow”, “America/New_York”, “Asia/Tokyo”. ZoneId si ottiene tramite il metodo statico of(String zoneId) o tramite il fuso orario predefinito del sistema.

ZoneId è diviso in due tipi: fixed offset (offset fisso, es. “+03:00”) e region-based (fusi regionali, es. “Europe/London”). I fusi regionali contengono regole di transizione dell’ora legale e modifiche storiche. Fixed offset è semplicemente un offset fisso.

Per ottenere l’offset corrente di un ZoneId in un momento specifico, utilizzare il metodo getRules(), che restituisce ZoneRules. ZoneRules contiene tutte le transizioni e gli offset per un determinato fuso. Questo è il meccanismo chiave per la corretta gestione del DST.

Tutti i fusi orari sono forniti con il JDK tramite i file tzdata (IANA Time Zone Database) e vengono aggiornati regolarmente. Su Android, la versione tzdata dipende dagli aggiornamenti di sistema tramite Google Play Services.

Creazione di ZonedDateTime

ZonedDateTime può essere creato in diversi modi. Il più semplice è now(), che restituisce l’ora corrente nel fuso orario di sistema. La variante now(ZoneId) consente di ottenere l’ora corrente in un fuso specificato.

Il metodo of(LocalDateTime, ZoneId) crea un ZonedDateTime dall’ora locale e dal fuso. La variante of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) crea dai componenti.

LocalDateTime può essere convertito in ZonedDateTime tramite il metodo atZone(ZoneId). Instant — tramite Instant.atZone(ZoneId). Date — tramite 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)

Conversione tra fusi orari

Il metodo principale di conversione è withZoneSameInstant(ZoneId). Converte un ZonedDateTime in un altro fuso orario mantenendo lo stesso momento. Ad esempio, 15:00 MSK → 8:00 EST. Il metodo withZoneSameLocal(ZoneId) cambia il fuso mantenendo l’ora locale — questo produce un momento diverso.

Per ottenere l’offset rispetto a UTC, utilizzare il metodo getOffset(), che restituisce ZoneOffset. ZoneOffset è una sottoclasse di ZoneId che rappresenta un offset fisso nel formato “+HH:mm” o “-HH:mm”.

La conversione in Instant viene effettuata tramite il metodo toInstant(). Instant è un momento assoluto nel tempo, indipendente dal fuso orario. La conversione inversa è 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"))

Lavorare con l’ora legale (DST)

Le transizioni dell’ora legale creano due problemi: vuoti (gaps) e sovrapposizioni (overlaps). Un vuoto si verifica in primavera quando gli orologi vengono spostati in avanti — un determinato orario non esiste. Una sovrapposizione si verifica in autunno quando gli orologi vengono spostati indietro — lo stesso orario si verifica due volte.

ZonedDateTime gestisce queste situazioni attraverso una strategia di risoluzione. Quando si crea un oggetto durante un vuoto, java.time sposta automaticamente l’ora dell’importo dell’offset. Quando si crea durante una sovrapposizione, viene selezionata la prima opzione (prima della transizione). Questo comportamento può essere modificato tramite withZoneSameInstant.

È possibile verificare se un’ora è in DST tramite zone.getRules().isDaylightSavings(instant). Il metodo getOffset() mostra l’offset effettivo per un determinato momento, e getRules().getDaylightSavings(instant) mostra l’importo di regolazione DST in millisecondi.

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("Ora legale", "Offset ora legale: $dstAmount")
    }
}

Formattazione di ZonedDateTime

Per formattare ZonedDateTime, utilizzare DateTimeFormatter. Il formato ISO standard include data, ora e offset: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. Formati predefiniti: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Per un output localizzato, utilizzare DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle può essere SHORT, MEDIUM, LONG, FULL. LONG include il nome del fuso (“MSK”), FULL include il nome completo (“Moscow Standard Time”).

Importante: durante l’analisi di una stringa con ZonedDateTime, il formato deve contenere informazioni sul fuso o sull’offset. Se il fuso non è specificato, utilizzare LocalDateTime.parse() e poi 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 in Android: esempi pratici

Il primo esempio — visualizzare l’ora di una riunione per l’utente nel suo fuso orario locale. Il server invia ZonedDateTime in UTC, il client converte nel fuso orario locale del dispositivo.

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

Il secondo esempio — calcolare il tempo fino al prossimo evento considerando il fuso orario. Utilizziamo ZonedDateTime per l’ora del server e Duration.between() per calcolare la differenza.

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

Il terzo esempio — lavorare con l’API Retrofit. Il server restituisce una stringa ISO-8601 con fuso. Utilizziamo un deserializzatore personalizzato per convertire in 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()
        )
    }
}

Errori comuni con i fusi orari

Il primo errore — utilizzare ZoneId.systemDefault() nel codice del server. Il fuso orario del server può differire da quello del client e l’utilizzo del fuso di sistema sul server porta a calcoli errati. Specificare sempre il fuso esplicitamente o utilizzare UTC come riferimento.

Il secondo errore — ignorare il DST nel calcolo della durata. Duration.between() gestisce correttamente le transizioni, ma se si sottraggono i timestamp manualmente, l’ora legale può causare un errore di 1 ora. Utilizzare ChronoUnit.HOURS.between() invece di calcoli manuali.

Il terzo errore — confondere withZoneSameInstant e withZoneSameLocal. Il primo cambia il fuso mantenendo il momento — l’ora si sposta. Il secondo cambia il fuso mantenendo l’ora locale — il momento cambia. Scegliere il metodo sbagliato è uno degli errori più comuni secondo SonarSource (2024).

Il quarto errore — presumere che il fuso orario del dispositivo sia sempre lo stesso del fuso orario dell’utente. L’utente potrebbe essere in viaggio e aspettarsi che l’app mostri l’ora nel suo fuso “di casa” piuttosto che nel fuso corrente. In questo caso, fornire la selezione del fuso tramite l’interfaccia.

Domande Frequenti

Qual è la differenza tra ZonedDateTime e OffsetDateTime?

ZonedDateTime contiene un identificatore di fuso regionale (es., “Europe/Moscow”) e gestisce il DST. OffsetDateTime memorizza solo un offset fisso (+03:00) senza regole regionali. Per la memorizzazione nel database, si consiglia OffsetDateTime.

Come ottenere l’ora corrente in UTC tramite ZonedDateTime?

Utilizzare ZonedDateTime.now(ZoneOffset.UTC) o Instant.now().atZone(ZoneOffset.UTC). Entrambe le opzioni restituiscono il momento corrente con offset zero. Per un timestamp semplice, utilizzare Instant.now() senza legame al fuso.

ZonedDateTime può essere serializzato tramite Gson o Moshi?

Sì, ma è necessario un adattatore personalizzato. Gson non supporta ZonedDateTime per impostazione predefinita. Moshi lo supporta tramite Rfc3339DateJsonAdapter. Si consiglia di utilizzare Kotlinx Serialization o la libreria JavaTimeModule per Jackson.

Come gestire la situazione quando l’ora cade in un vuoto DST?

java.time sposta automaticamente l’ora in avanti dell’importo dell’offset. Ad esempio, se le 02:30 non esistono quando gli orologi vengono spostati alle 03:00, ZonedDateTime creerà un oggetto alle 03:30. È possibile verificare la presenza di un vuoto tramite ZoneRules.getTransition(instant).

Perché ZonedDateTime non è raccomandato per i database SQL?

JDBC 4.2 supporta OffsetDateTime ma non ZonedDateTime direttamente. ZonedDateTime contiene un fuso regionale che non ha equivalente in SQL. Si consiglia di memorizzare OffsetDateTime o Instant e di memorizzare il fuso in una colonna separata.

Riepilogo

  • ZonedDateTime è una classe immutabile per data e ora con fuso orario, che gestisce correttamente il DST tramite il database IANA Time Zone Database.
  • La differenza principale rispetto a LocalDateTime è la presenza di un fuso, che rende ZonedDateTime un identificatore univoco di un momento nel tempo.
  • Per la conversione tra fusi, utilizzare withZoneSameInstant(), che preserva il momento, non withZoneSameLocal.
  • Durante le transizioni dell’ora legale, java.time risolve automaticamente i vuoti e le sovrapposizioni tramite regole di fuso integrate.
  • Per la memorizzazione nel database, utilizzare OffsetDateTime o memorizzare Instant e ZoneId separatamente.
  • Su Android, per convertire ZonedDateTime nell’ora locale del dispositivo, utilizzare ZoneId.systemDefault() insieme a withZoneSameInstant.
  • Per la serializzazione JSON, è necessario un adattatore personalizzato — utilizzare Kotlinx Serialization o Jackson JavaTimeModule.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche