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 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.
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.
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.
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).
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)
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).
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"))
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.
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")
}
}
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().
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
)
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.
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.
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.
data class EventResponse(
@JsonAdapter(ZonedDateTimeAdapter::class)
val eventTime: ZonedDateTime
)
class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
override fun fromJson(reader: JsonReader): ZonedDateTime? {
return ZonedDateTime.parse(
reader.nextString()
)
}
}
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
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.
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.
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.
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).
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
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.
Leia também