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):
atZoneretorna o horário após a transição.LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York"))se torna03: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):
atZoneretorna o mais cedo dos dois momentos válidos (o que ocorre antes da mudança de offset). UsewithEarlierOffsetAtOverlap()ouwithLaterOffsetAtOverlap()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 hoursEm 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.
| Objetivo | Mé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 + OffsetA distinção é clara:
isBefore/isAfter/isEqualcomparam os momentos subjacentes (Instants).equalscompara a estrutura completa — doisZonedDateTimes que representam o mesmo momento, mas têm fusos diferentes, não sãoequal.
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.
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ãowithZoneSameLocal(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)foifalsemesmo que os dois representassem o mesmo momento.equalscompara a estrutura completa (data-hora local + fuso). Para "mesmo momento independentemente de como está rotulado," useisEqualou compareInstants. Esta é exatamente a mesma distinção queLocalDate.equalsvsisEqualfazia —equalspara "mesmo objeto de valor,"isEqualpara "mesmo ponto no tempo."- A lacuna de horário de verão (
2025-03-09 02:30em NY) foi resolvida avançando para03: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, useZoneRules.getTransition(localDateTime)e verifique se o objeto retornado é uma lacuna. - A sobreposição de horário de verão (
2025-11-02 01:30em NY) gerou doisZonedDateTimes distintos com os mesmos campos locais e offsets diferentes —EDTvsEST, com uma hora de diferença.withLaterOffsetAtOverlap()ewithEarlierOffsetAtOverlap()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)eplus(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. UseplusDays/plusWeekspara agendamento no formato de calendário ("mesmo horário amanhã") eplus(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.