W3docs

Java ZonedDateTime

Represente datas e horas com fuso horário em Java usando ZonedDateTime e a classe ZoneId.

ZonedDateTime é um LocalDateTime com um ZoneId associado. Ele diz: "esta data e hora no calendário, neste lugar." A combinação identifica um único momento na linha do tempo global — 2025-11-04T14:00 [America/New_York] é exatamente um Instant, distinto de 2025-11-04T14:00 [Europe/Berlin].

Esta é a classe que você utiliza sempre que o horário local de um evento em um lugar específico importa. Calendários de reuniões. Tarefas agendadas que precisam ser executadas às "9h no fuso do usuário." Qualquer coisa que precise sobreviver a uma transição de horário de verão. LocalDateTime não tem informação suficiente; Instant está em UTC e não carrega o rótulo de fuso com significado humano. ZonedDateTime é os dois ao mesmo tempo.

ZoneId: o catálogo de fusos horários

Antes de ZonedDateTime, conheça o próprio ZoneId — um fuso é identificado por um ZoneId, que você obtém com 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();

As strings são identificadores do Banco de Dados de Fusos Horários da IANA (Região/Cidade). A lista completa está em ZoneId.getAvailableZoneIds() — cerca de 600 entradas, atualizadas periodicamente conforme os países alteram seus fusos ou regras de horário de verão. ZoneId carrega o registro histórico da IANA, portanto datas de 1985 utilizam as regras que estavam em vigor em 1985.

Evite ZoneOffset (um ±HH:MM fixo) quando você quer um fuso real. ZoneOffset.of("-05:00") está correto para Nova York em novembro e errado em junho; ZoneId.of("America/New_York") está correto durante todo o ano.

Nomes de fuso com três letras como "EST" e "PST" são hoje em dia principalmente aliases, ambíguos (era Eastern Standard ou Eastern Australia?) e silenciosamente descontinuados. Use Região/Cidade. "UTC" e "GMT" são casos especiais e são aceitos.

Criação

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]");

O caminho de construção mais comum é "tenho um LocalDateTime, tenho um ZoneId, associá-los":

LocalDateTime ldt = LocalDateTime.of(2025, 11, 4, 14, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));

atZone(zone) é a ponte de uma chamada de uma leitura de relógio local para um momento com fuso. Ele também lida com os dois casos extremos que o horário de verão introduz.

Horário de verão: quando o relógio avança ou repete

Duas vezes por ano, o relógio em qualquer fuso que observa o horário de verão avança ou repete. Quando avança — nos EUA, 02:00 salta para 03:00 em um domingo de março — os horários entre 02:00 e 03:00 não existem naquele dia. Quando recua, os horários entre 01:00 e 02:00 acontecem duas vezes. ZonedDateTime precisa lidar com ambos os casos, e o que ele faz está documentado:

  • Horário ignorado (lacuna): atZone retorna o horário após a transição. LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York")) se torna 03:30-04:00 — o JDK avançou uma hora para chegar em um horário de relógio válido.
  • Horário repetido (sobreposição): atZone retorna o mais cedo dos dois momentos válidos (o que ocorre antes da mudança de offset). Use withEarlierOffsetAtOverlap() ou withLaterOffsetAtOverlap() para escolher explicitamente.
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)

Os dois ZonedDateTimes têm o mesmo LocalDateTime, mas offsets diferentes e Instants diferentes. Este é o único lugar em java.time onde a mesma leitura de relógio local se mapeia legitimamente para dois momentos — e é a fonte dos bugs relacionados ao horário de verão que você já ouviu falar. Seja deliberado quando a sobreposição importa.

Decomposição

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();

Os acessores se dividem em três grupos: a metade do fuso (getZone, getOffset), a metade do relógio local (toLocalDateTime, toLocalDate, toLocalTime) e a metade do momento global (toInstant). Os três são simultaneamente verdadeiros para o mesmo ZonedDateTime; você escolhe a projeção que precisa.

OffsetDateTime é um tipo relacionado — LocalDateTime mais um ZoneOffset (sem fuso, sem horário de verão). É útil para serializar "2025-11-04T14:00-05:00" sem se comprometer com um fuso nomeado (frequentemente o que os timestamps JSON precisam); para qualquer código que precise de aritmética com consciência de horário de verão, mantenha o ZonedDateTime.

Dois sabores de "próximo dia"

ZonedDateTime tem dois métodos que parecem similares e não são:

zdt.plusDays(1);                                              // add 1 day to the local clock reading
zdt.plus(Duration.ofHours(24));                                // add exactly 24 hours

Em um dia de transição de horário de verão, os dois divergem. No dia em que os relógios avançam, plusDays(1) chega ao mesmo horário local amanhã (que é apenas 23 horas de tempo real). plus(Duration.ofHours(24)) chega a um horário de relógio uma hora mais tarde que ontem.

ObjetivoMétodo
"Mesmo horário amanhã" (calendário)plusDays(1)
"Exatamente 24 horas a partir de agora" (duração)plus(Duration.ofHours(24))

Ambos estão corretos; eles respondem a perguntas diferentes. Escolha deliberadamente.

Comparações e igualdade

zdt1.isBefore(zdt2);                                          // compares Instants
zdt1.isAfter(zdt2);
zdt1.isEqual(zdt2);                                           // compares Instants
zdt1.equals(zdt2);                                            // compares LocalDateTime + Zone + Offset

A distinção é clara:

  • isBefore/isAfter/isEqual comparam os momentos subjacentes (Instants).
  • equals compara a estrutura completa — dois ZonedDateTimes que representam o mesmo momento, mas têm fusos diferentes, não são equal.

Para "estes são o mesmo momento independentemente do fuso," use isEqual ou converta ambos para Instant e compare.

Um exemplo prático: uma reunião em três escritórios

O programa abaixo agenda uma reunião para 14:00 no horário de Berlim e calcula que horas são nos escritórios de Nova York e Tóquio. Em seguida, agenda uma reunião semanal recorrente que sobrevive a uma transição de horário de verão, demonstrando a diferença entre plusDays(7) e plus(Duration.ofDays(7)) em uma semana de transição.

java— editable, runs on the server

O que extrair da execução:

  • withZoneSameInstant(otherZone) é a operação para "que horas são no escritório deles?" — mantém o momento fixo e exibe novamente o relógio de parede no novo fuso. Seu irmão withZoneSameLocal(otherZone) mantém o relógio de parede e muda o momento (a reunião se move). Os nomes são confundíveis; a diferença é qual das coisas permanece igual. Leia-os com atenção ao escrever.
  • berlin.equals(ny) foi false mesmo que os dois representassem o mesmo momento. equals compara a estrutura completa (data-hora local + fuso). Para "mesmo momento independentemente de como está rotulado," use isEqual ou compare Instants. Esta é exatamente a mesma distinção que LocalDate.equals vs isEqual fazia — equals para "mesmo objeto de valor," isEqual para "mesmo ponto no tempo."
  • A lacuna de horário de verão (2025-03-09 02:30 em NY) foi resolvida avançando para 03:30-04:00. O JDK não lançou exceção; ele escolheu o momento pós-transição. Se você absolutamente precisar detectar que forneceu um horário impossível, use ZoneRules.getTransition(localDateTime) e verifique se o objeto retornado é uma lacuna.
  • A sobreposição de horário de verão (2025-11-02 01:30 em NY) gerou dois ZonedDateTimes distintos com os mesmos campos locais e offsets diferentes — EDT vs EST, com uma hora de diferença. withLaterOffsetAtOverlap() e withEarlierOffsetAtOverlap() são como você escolhe. Se estiver armazenando eventos agendados, decida antecipadamente qual o usuário quer dizer e aplique a chamada correta no momento da análise.
  • plusDays(1) e plus(Duration.ofHours(24)) produziram resultados diferentes no dia do avanço do horário de verão — 23 horas de tempo real de diferença vs 24 horas, chegando a horários de relógio diferentes. Use plusDays/plusWeeks para agendamento no formato de calendário ("mesmo horário amanhã") e plus(Duration) para aritmética de tempo decorrido ("alarme em 24 horas"). A escolha quase sempre reflete a intenção voltada ao usuário.

O que vem a seguir

ZonedDateTime é o lado amigável para humanos de "um momento com um rótulo." O próximo capítulo, Java Instant, é o lado amigável para máquinas — um momento como nanossegundos desde a época, sem fuso, sem calendário, o tipo que todo sistema distribuído usa na comunicação.

Prática

Prática
Você agenda uma reunião recorrente às 09:00 America/New_York e armazena a próxima ocorrência com `nextMeeting.plusDays(7)`. Na semana que cruza a transição de horário de verão do avanço, o que é verdade sobre o resultado?
Você agenda uma reunião recorrente às 09:00 America/New_York e armazena a próxima ocorrência com `nextMeeting.plusDays(7)`. Na semana que cruza a transição de horário de verão do avanço, o que é verdade sobre o resultado?
Was this page helpful?