CodeGym /Cursos /JAVA 25 SELF /ZonedDateTime, Instant e trabalho com fusos horários

ZonedDateTime, Instant e trabalho com fusos horários

JAVA 25 SELF
Nível 13 , Lição 3
Disponível

1. Conceito de fuso horário

Há muitos fusos horários no mundo: quando é meio-dia em Minsk, em Nova York ainda é manhã, e em Tóquio já é noite. Se você armazena data e hora sem considerar o fuso horário, é fácil gerar confusão: por exemplo, se o seu servidor está na Alemanha e o usuário — em Vladivostok, a exibição do horário "2025-06-01 12:00" significará algo completamente diferente para cada um.

Fuso horário (timezone) — é a regra que determina quanto tempo deve ser somado ou subtraído do tempo de Greenwich (UTC) para obter a hora “local” de uma região específica.

Em Java, para trabalhar com fusos horários utiliza-se a classe ZoneId. Aqui estão alguns exemplos de identificadores de zona:

  • "Europe/Minsk"
  • "UTC"
  • "America/New_York"
  • "Asia/Tokyo"

Por que isso é importante?

  • Exibição correta do horário para usuários de diferentes países.
  • Registro adequado do horário de eventos (por exemplo, logging, compra de passagens, prazos).
  • Considerar a mudança para horário de verão/inverno (obrigado, Europa!).

2. ZonedDateTime — data e hora com fuso horário

ZonedDateTime é uma classe que armazena a data, a hora e a informação do fuso horário. É como LocalDateTime, só que também “sabe” em que região está.

Criação de ZonedDateTime

Data e hora atuais no fuso do sistema

import java.time.ZonedDateTime;

ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // Por exemplo: 2025-06-01T15:30:00+03:00[Europe/Minsk]

Hora em um fuso horário específico

import java.time.ZoneId;

ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));

System.out.println("Minsk: " + MinskTime);
System.out.println("Nova York: " + newYorkTime);

Criação a partir de LocalDateTime

import java.time.LocalDateTime;

LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]

Obter e definir o fuso horário

ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("Tóquio: " + tokyoTime);

Conversão entre zonas: withZoneSameInstant()

Às vezes é preciso saber como o mesmo evento se apresenta em outro fuso. Para isso, usamos withZoneSameInstant():

ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));

System.out.println("Horário da reunião em Minsk: " + MinskMeeting);
System.out.println("O mesmo evento em Nova York: " + newYorkMeeting);

Atenção: withZoneSameInstant() converte o horário de modo que ele corresponda ao mesmo instante em outro fuso. Se usar withZoneSameLocal(), a data e a hora permanecerão as mesmas e o fuso mudará — isso quase sempre é um erro!

3. Instant — ponto absoluto no tempo

Instant é uma classe que representa um instante absoluto, independentemente do fuso horário. Tecnicamente, é a quantidade de segundos e nanossegundos transcorridos desde 1º de janeiro de 1970 em Greenwich (UTC). Se o tempo tivesse um passaporte, o Instant seria o número dele.

Criação de Instant

import java.time.Instant;

Instant now = Instant.now();
System.out.println(now); // Por exemplo: 2025-06-01T12:30:00.123Z

Observe a letra Z — significa “Zulu time”, ou seja, UTC.

Criação a partir de segundos desde a época Unix

Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z

Conversão de InstantZonedDateTime/LocalDateTime

De ZonedDateTime para Instant

ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);

De Instant para ZonedDateTime

ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);

De Instant para LocalDateTime

import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;

LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);

4. Prática: hora atual em diferentes fusos, conversão entre zonas

Obtendo a hora atual em diferentes fusos

Vamos fazer um miniaplicativo que mostre a hora atual em Minsk, Nova York e Tóquio:

import java.time.ZonedDateTime;
import java.time.ZoneId;

public class TimeZonesDemo {
    public static void main(String[] args) {
        ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
        ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
        ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

        System.out.println("Minsk:    " + Minsk);
        System.out.println("Nova York:  " + newYork);
        System.out.println("Tóquio:     " + tokyo);
    }
}

Conversão de horário entre zonas

Suponha que você tenha um evento marcado para 18:00 em Minsk. Como saber que horas serão em Nova York e em Tóquio?

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class MeetingTime {
    public static void main(String[] args) {
        LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
        ZonedDateTime minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));

        ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
        ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

        System.out.println("Reunião em Minsk:   " + minskEvent);
        System.out.println("Em Nova York:        " + newYorkEvent);
        System.out.println("Em Tóquio:            " + tokyoEvent);
    }
}

Conversão de LocalDateTime em ZonedDateTime e vice-versa

Local → Zoned:

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(zonedTime);

Zoned → Local:

LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);

5. Observações e nuances importantes

Por que não armazenar apenas LocalDateTime

LocalDateTime é apenas data e hora sem fuso horário. Para a maioria das regras de negócio isso não é suficiente! Por exemplo, se você armazena "2025-06-01 12:00" como LocalDateTime, para um usuário em Minsk e outro em Nova York serão instantes completamente diferentes no tempo real.

Sempre armazene o tempo absoluto (por exemplo, Instant) ou o tempo com fuso (ZonedDateTime) se o evento estiver realmente vinculado a um fuso específico. LocalDateTime é bom apenas quando você trabalha com datas “flutuantes” (por exemplo, aniversário sem considerar o horário do dia e o fuso).

Problemas na mudança para horário de verão/inverno

Fusos horários não são apenas o deslocamento em relação ao UTC, mas também regras de mudança para horário de verão/inverno. Por exemplo, em alguns países, em um determinado dia, os relógios são adiantados ou atrasados em uma hora — e, se você armazena apenas LocalDateTime, você não saberá se aquele horário existiu de fato.

Exemplo de “buraco no tempo”:

  • Nos EUA, em março, às 2:00 da madrugada, os relógios pulam para 3:00.
  • O horário "2025-03-10 02:30" em Nova York não existiu!

Trabalhando com ZonedDateTime, você fica protegido dessas surpresas: a biblioteca verifica a validade do horário por conta própria.

Esquema: como se relacionam LocalDateTime, ZonedDateTime, Instant

graph TD
    A["LocalDateTime 
(data + hora, sem fuso)"] -->|+ ZoneId| B["ZonedDateTime
(data + hora + fuso)"] B -->|"toInstant()"| C["Instant
(tempo absoluto, UTC)"] C -->|"atZone(ZoneId)"| B B -->|"toLocalDateTime()"| A

6. Erros comuns ao trabalhar com ZonedDateTime e Instant

Erro nº 1: Usar LocalDateTime para eventos globais.
Se você armazena data e hora de reuniões de usuários de países diferentes como LocalDateTime, cada um verá seu “12:00”, apesar de serem instantes diferentes. Para eventos globais, use ZonedDateTime ou Instant.

Erro nº 2: Ignorar o fuso horário ao fazer parsing de string.
Se você faz o parsing da string "2025-06-01T12:00:00" sem informar o fuso, você obterá um LocalDateTime, e não um ZonedDateTime. Para obter ZonedDateTime, use strings com fuso ou adicione-o explicitamente.

Erro nº 3: Conversão incorreta entre zonas.
Usar withZoneSameLocal() em vez de withZoneSameInstant() pode levar a horários incorretos. Sempre use withZoneSameInstant() se quiser obter o mesmo instante em outro fuso.

Erro nº 4: Não considerar a mudança para horário de verão/inverno.
Se você planeja eventos na fronteira da mudança, use necessariamente ZonedDateTime e confie na biblioteca — ela conhece todas as mudanças e “buracos” no tempo.

Erro nº 5: Comparar ZonedDateTime sem considerar o fuso.
Dois ZonedDateTime com fusos diferentes, mas o mesmo horário local, podem representar instantes diferentes. Para comparar, use toInstant().

1
Tarefa
JAVA 25 SELF, nível 13, lição 3
Bloqueado
Centro de Comando Global: horário ao redor do mundo 🌍
Centro de Comando Global: horário ao redor do mundo 🌍
1
Tarefa
JAVA 25 SELF, nível 13, lição 3
Bloqueado
Conferência internacional: fixação do evento em um fuso horário 🌐
Conferência internacional: fixação do evento em um fuso horário 🌐
1
Tarefa
JAVA 25 SELF, nível 13, lição 3
Bloqueado
Movimentação do teletransporte: um evento em fusos horários diferentes 🚀
Movimentação do teletransporte: um evento em fusos horários diferentes 🚀
1
Tarefa
JAVA 25 SELF, nível 13, lição 3
Bloqueado
Crônicas do tempo: viagem através do Instant 🌌
Crônicas do tempo: viagem através do Instant 🌌
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION