ZonedDateTime — o que é, trabalhando com fusos horários e datas

Autor: IT Sectr Publicado: 2026-07-13 Tempo de leitura: 10 min

ZonedDateTime é uma classe imutável do pacote java.time que armazena data e hora juntamente com informações de fuso horário (ZoneId). Ao contrário de LocalDateTime, ZonedDateTime identifica de forma inequívoca um momento na linha do tempo. De acordo com a especificação da Oracle Java 17 (2024), a classe lida corretamente com as transições de horário de verão (DST) através das regras de zona do banco de dados IANA Time Zone Database.

Pontos Principais

  • ZonedDateTime é uma classe imutável que combina data, hora e fuso horário (ZoneId) em um único objeto.
  • Ao contrário de LocalDateTime, ZonedDateTime define de forma inequívoca um momento na linha do tempo e é adequado para sistemas globais.
  • A classe lida automaticamente com as transições de horário de verão (DST) de acordo com as regras do banco de dados IANA Time Zone Database.
  • Para converter entre fusos horários, use o método withZoneSameInstant(ZoneId).
  • Armazenar ZonedDateTime em bancos de dados é recomendado via OffsetDateTime ou TIMESTAMP WITH TIME ZONE.

O que é ZonedDateTime?

ZonedDateTime é uma das principais classes do pacote java.time, representando data e hora com informações completas de fuso horário. Ela combina três componentes: LocalDateTime (data e hora), ZoneId (identificador de zona) e ZoneOffset (deslocamento relativo ao UTC).

Ao contrário de LocalDateTime, que armazena apenas o horário local (wall-clock time) sem vinculação de zona, ZonedDateTime identifica de forma inequívoca um momento. Duas instâncias idênticas de LocalDateTime em fusos horários diferentes representam momentos distintos. Duas instâncias idênticas de ZonedDateTime — o mesmo momento.

A classe é completamente imutável e thread-safe. Todas as operações aritméticas retornam um novo objeto. ZonedDateTime implementa a interface ChronoZonedDateTime e pode ser usada sempre que for necessário trabalhar com tempo zonal em Java.

De acordo com as especificações do Oracle Java 17, ZonedDateTime suporta trabalhar com qualquer zona do banco de dados IANA Time Zone Database, que inclui mais de 600 fusos horários.

ZonedDateTime vs LocalDateTime: qual a diferença?

A principal diferença — ZonedDateTime contém um fuso horário, enquanto LocalDateTime não. Esta diferença fundamental determina o escopo de cada classe.

LocalDateTime é usado para eventos locais: horário de concerto, horário de aulas, data de nascimento. Se um evento ocorre em Moscou às 15:00, LocalDateTime registrará 15:00 sem qualquer vinculação. Se você mover o servidor para Nova York, a hora permanecerá 15:00 — mas será um momento físico diferente.

ZonedDateTime é usado para dados globais: logs de servidor, carimbos de data/hora em APIs, reuniões internacionais. Se uma reunião está agendada para as 15:00 MSK, ZonedDateTime preservará tanto a hora quanto a zona. Em Nova York, será exibido corretamente como 8:00 EST. De acordo com Baeldung (2024), escolher entre LocalDateTime e ZonedDateTime é a decisão arquitetônica mais comum ao trabalhar com datas.

Regra prática: se os dados são armazenados para uma única região — use LocalDateTime. Se os dados cruzam limites de fusos horários — use ZonedDateTime. Se você precisa representar um momento absoluto — use Instant.

Como funciona o fuso horário em java.time?

O fuso horário em java.time é representado pela classe ZoneId. ZoneId é um identificador de zona no formato “continente/região”, por exemplo “Europe/Moscow”, “America/New_York”, “Asia/Tokyo”. ZoneId é obtido através do método estático of(String zoneId) ou através do fuso horário padrão do sistema.

ZoneId é dividido em dois tipos: fixed offset (deslocamento fixo, ex. “+03:00”) e region-based (zonas regionais, ex. “Europe/London”). As zonas regionais contêm regras de transição de horário de verão e mudanças históricas. Fixed offset é simplesmente um deslocamento fixo.

Para obter o deslocamento atual de um ZoneId num momento específico, use o método getRules(), que retorna ZoneRules. ZoneRules contém todas as transições e deslocamentos para uma determinada zona. Este é o mecanismo chave para o correto manuseio de DST.

Todos os fusos horários são fornecidos com o JDK através dos arquivos tzdata (IANA Time Zone Database) e são atualizados regularmente. No Android, a versão tzdata depende das atualizações do sistema através do Google Play Services.

Criação de ZonedDateTime

ZonedDateTime pode ser criado de várias formas. A mais simples é now(), que retorna a hora atual no fuso horário do sistema. A variante now(ZoneId) permite obter a hora atual numa zona especificada.

O método of(LocalDateTime, ZoneId) cria um ZonedDateTime a partir de hora local e zona. A variante of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) cria a partir de componentes.

LocalDateTime pode ser convertido para ZonedDateTime através do método atZone(ZoneId). Instant — através de Instant.atZone(ZoneId). Date — através de 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)

Conversão entre fusos horários

O principal método de conversão é withZoneSameInstant(ZoneId). Ele converte um ZonedDateTime para outro fuso horário mantendo o mesmo momento. Por exemplo, 15:00 MSK → 8:00 EST. O método withZoneSameLocal(ZoneId) muda a zona mantendo a hora local — isto produz um momento diferente.

Para obter o deslocamento relativo ao UTC, use o método getOffset(), que retorna ZoneOffset. ZoneOffset é uma subclasse de ZoneId que representa um deslocamento fixo no formato “+HH:mm” ou “-HH:mm”.

A conversão para Instant é feita através do método toInstant(). Instant é um momento absoluto no tempo, independente do fuso horário. A conversão 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"))

Trabalhar com horário de verão (DST)

As transições de horário de verão criam dois problemas: lacunas (gaps) e sobreposições (overlaps). Uma lacuna ocorre na primavera quando os relógios são adiantados — um determinado horário não existe. Uma sobreposição ocorre no outono quando os relógios são atrasados — o mesmo horário ocorre duas vezes.

ZonedDateTime lida com estas situações através de uma estratégia de resolução. Ao criar um objeto durante uma lacuna, java.time desloca automaticamente a hora pelo valor do deslocamento. Ao criar durante uma sobreposição, a primeira opção (antes da transição) é selecionada. Este comportamento pode ser alterado através de withZoneSameInstant.

Você pode verificar se um horário está em DST através de zone.getRules().isDaylightSavings(instant). O método getOffset() mostra o deslocamento real para um determinado momento, e getRules().getDaylightSavings(instant) mostra o ajuste de DST em milissegundos.

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("Horário de Verão", "Deslocamento horário de verão: $dstAmount")
    }
}

Formatação de ZonedDateTime

Para formatar ZonedDateTime, use DateTimeFormatter. O formato ISO padrão inclui data, hora e deslocamento: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. Formatos predefinidos: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Para saída localizada, use DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle pode ser SHORT, MEDIUM, LONG, FULL. LONG inclui o nome da zona (“MSK”), FULL inclui o nome completo (“Moscow Standard Time”).

Importante: ao analisar uma string com ZonedDateTime, o formato deve conter informações de zona ou deslocamento. Se a zona não for especificada, use LocalDateTime.parse() e depois 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 no Android: exemplos práticos

O primeiro exemplo — exibir o horário de uma reunião para o usuário no seu fuso horário local. O servidor envia ZonedDateTime em UTC, o cliente converte para o fuso horário local do 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)
}

O segundo exemplo — calcular o tempo até o próximo evento considerando o fuso horário. Usamos ZonedDateTime para o horário do servidor e Duration.between() para calcular a diferença.

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

O terceiro exemplo — trabalhar com a API Retrofit. O servidor retorna uma string ISO-8601 com zona. Usamos um desserializador personalizado para converter para 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()
        )
    }
}

Erros comuns ao trabalhar com fusos horários

O primeiro erro — usar ZoneId.systemDefault() no código do servidor. O fuso horário do servidor pode diferir do cliente, e usar o fuso do sistema no servidor leva a cálculos incorretos. Sempre especifique a zona explicitamente ou use UTC como referência.

O segundo erro — ignorar o DST ao calcular a duração. Duration.between() lida corretamente com as transições, mas se você subtrair timestamps manualmente, o horário de verão pode causar um erro de 1 hora. Use ChronoUnit.HOURS.between() em vez de cálculos manuais.

O terceiro erro — confundir withZoneSameInstant e withZoneSameLocal. O primeiro muda a zona mantendo o momento — a hora é deslocada. O segundo muda a zona mantendo a hora local — o momento muda. Escolher o método errado é um dos erros mais comuns de acordo com SonarSource (2024).

O quarto erro — assumir que o fuso horário do dispositivo é sempre o mesmo que o fuso horário do usuário. O usuário pode estar viajando e esperar que o aplicativo mostre a hora no seu fuso horário “de origem” em vez do atual. Neste caso, forneça seleção de zona através da interface.

Perguntas Frequentes

Qual a diferença entre ZonedDateTime e OffsetDateTime?

ZonedDateTime contém um identificador de zona regional (ex., “Europe/Moscow”) e lida com DST. OffsetDateTime armazena apenas um deslocamento fixo (+03:00) sem regras regionais. Para armazenamento em banco de dados, OffsetDateTime é recomendado.

Como obter a hora atual em UTC via ZonedDateTime?

Use ZonedDateTime.now(ZoneOffset.UTC) ou Instant.now().atZone(ZoneOffset.UTC). Ambas as opções retornam o momento atual com deslocamento zero. Para um timestamp simples, use Instant.now() sem vinculação de zona.

ZonedDateTime pode ser serializado via Gson ou Moshi?

Sim, mas é necessário um adaptador personalizado. Gson não suporta ZonedDateTime por padrão. Moshi suporta através do Rfc3339DateJsonAdapter. Recomenda-se usar Kotlinx Serialization ou a biblioteca JavaTimeModule para Jackson.

Como lidar com a situação quando o horário cai numa lacuna de DST?

java.time desloca automaticamente a hora para a frente pelo valor do deslocamento. Por exemplo, se 02:30 não existe quando os relógios são adiantados para 03:00, ZonedDateTime criará um objeto às 03:30. Você pode verificar a existência de uma lacuna através de ZoneRules.getTransition(instant).

Por que ZonedDateTime não é recomendado para bancos de dados SQL?

JDBC 4.2 suporta OffsetDateTime mas não ZonedDateTime diretamente. ZonedDateTime contém uma zona regional que não tem equivalente em SQL. Recomenda-se armazenar OffsetDateTime ou Instant, e armazenar a zona numa coluna separada.

Resumo

  • ZonedDateTime é uma classe imutável para data e hora com fuso horário, que lida corretamente com DST através do banco de dados IANA Time Zone Database.
  • A principal diferença do LocalDateTime é a presença de uma zona, tornando ZonedDateTime um identificador inequívoco de um momento no tempo.
  • Para conversão entre zonas, use withZoneSameInstant(), que preserva o momento, não withZoneSameLocal.
  • Durante as transições de horário de verão, java.time resolve automaticamente lacunas e sobreposições através de regras de zona integradas.
  • Para armazenamento em banco de dados, use OffsetDateTime ou armazene Instant e ZoneId separadamente.
  • No Android, para converter ZonedDateTime para a hora local do dispositivo, use ZoneId.systemDefault() em conjunto com withZoneSameInstant.
  • Para serialização JSON, é necessário um adaptador personalizado — use Kotlinx Serialization ou Jackson JavaTimeModule.

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também