Introdução à API de Data e Hora do Java
Introdução à moderna API de data e hora do Java em java.time, substituindo as classes legadas Date e Calendar.
O Java 8 adicionou java.time, um novo pacote para representar datas, horas, durações, fusos horários e a aritmética entre eles. Ele substituiu duas APIs anteriores — java.util.Date e java.util.Calendar — que tinham uma reputação merecida de ser o canto mais mal projetado do JDK. A nova API foi impulsionada pela biblioteca Joda-Time anterior de Stephen Colebourne; se você já usou Joda, java.time parecerá familiar.
As duas coisas importantes sobre o redesenho:
- Todo tipo é imutável. Um
LocalDateuma vez criado nunca muda. Métodos comoplusDays(7)retornam um novoLocalDate. Isso torna a API thread-safe por construção e elimina toda uma categoria de bugs. - Cada tipo significa uma coisa.
LocalDateé uma data sem hora.Instanté um momento na linha do tempo.Durationé uma duração de tempo. ODatelegado era de alguma forma tudo isso ao mesmo tempo, dependendo de qual construtor você usava; a nova API os separa para que o tipo indique que tipo de valor você tem.
Este capítulo é o mapa. Os próximos dez capítulos aprofundam cada classe.
Os tipos principais
"A date" LocalDate 2025-11-04
"A time of day" LocalTime 14:30:00
"Both, no zone" LocalDateTime 2025-11-04T14:30:00
"Both, with zone" ZonedDateTime 2025-11-04T14:30:00-05:00 [America/New_York]
"A moment" Instant 2025-11-04T19:30:00Z (UTC, seconds-since-epoch)
"A length of time" Duration PT1H30M (1 hour 30 minutes)
"A length of date" Period P1Y2M3D (1 year 2 months 3 days)A divisão horizontal — Local* vs Zoned/Instant — é a mais importante. Os tipos Local não carregam fuso horário. Um LocalDate de 2025-11-04 é "o quarto de novembro"; ele não diz se é o quarto em Tóquio ou em Honolulu. É o tipo certo para uma data de aniversário, uma data de contrato ou um seletor de datas na interface.
Os tipos com zona carregam seu fuso. ZonedDateTime é "este instante no calendário neste lugar," que é o que você quer para "reunião agendada para 9h em Nova York." Instant é um momento na linha do tempo global — segundos UTC desde a época — que é o que você quer para registros de log, timestamps de mensagens, qualquer coisa que precise ser ordenada globalmente sem precisar de rótulos locais.
A divisão horizontal entre Duration e Period também importa. Duration é uma duração de tempo que você pode comparar em segundos — PT24H é exatamente 24 × 3600 segundos. Period é uma duração expressa em termos de calendário — P1M (um mês) tem 30 dias em alguns meses e 31 em outros. Para medições de tempo, você quer Duration. Para "adicionar um mês a uma data de cobrança," você quer Period.
A forma fluente
Todo tipo é construído e modificado por meio de uma API fluente consistente:
LocalDate today = LocalDate.now();
LocalDate stardate = LocalDate.of(2025, 11, 4);
LocalDate parsed = LocalDate.parse("2025-11-04");
LocalDate nextWeek = today.plusDays(7); // immutable: returns a NEW LocalDate
LocalDate lastYear = today.minusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1); // with* returns a copy with one field changed
boolean before = today.isBefore(stardate);
int year = today.getYear();Três formatos que você verá em todo lugar:
now()— valor atual do relógio do sistema.of(...)— componentes explícitos.parse(...)— a partir de uma string (ISO-8601 por padrão).
E para transformações:
plusX(n)/minusX(n)— aritmética.withX(value)— substitui um único campo.isBefore(other)/isAfter(other)— comparação.
Esse formato se repete em LocalDate, LocalTime, LocalDateTime, ZonedDateTime e Instant. Uma vez que você conhece o padrão, cada classe fala o mesmo dialeto com um vocabulário ligeiramente diferente.
Fusos horários são difíceis, e a API admite isso
O maior motivo pelo qual java.util.Date era problemático é que ele tentava tornar os fusos horários invisíveis. O resultado foi a famosa classe de bug de "armazenar um Date, recuperá-lo em um servidor em um fuso horário diferente e obter a data do calendário errada." java.time resolve isso tornando o fuso explícito no tipo.
Se você aceitar uma data de um usuário e não souber em qual fuso ele está, armazene como LocalDate. Se ele disser que é "9h no horário dele" e você conhecer o fuso, armazene como ZonedDateTime com o fuso. Se você registrar um evento de servidor, armazene como Instant. Não armazene um LocalDateTime esperando que o fuso horário apareça; o fuso ausente é exatamente o bug.
Instant now = Instant.now(); // unambiguous: a moment in UTC
ZonedDateTime localized = now.atZone(ZoneId.of("Europe/Berlin")); // a label for that moment in BerlinA hierarquia de fusos:
ZoneOffseté um deslocamento fixo±HH:MMem relação ao UTC:+05:30,-08:00. Sem tratamento de horário de verão.ZoneIdé um fuso nomeado:Europe/Berlin,America/New_York. Carrega o registro do banco de dados IANA sobre qual deslocamento aquele fuso tem em qualquer dia específico, incluindo transições de horário de verão e mudanças históricas.
Sempre prefira ZoneId em vez de ZoneOffset quando tiver escolha. "America/New_York" está correto durante o horário de verão e fora dele; "−05:00" está correto apenas fora do horário de verão.
Os tipos legados não desapareceram
java.util.Date, java.util.Calendar e java.text.SimpleDateFormat ainda existem. Código novo não deveria usá-los — mas muito código antigo usa, e você precisará interoperar. Os métodos de conversão são diretos:
// java.util.Date <-> java.time.Instant
Instant inst = legacyDate.toInstant();
Date back = Date.from(inst);
// java.util.Calendar -> java.time.ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(
cal.toInstant(), cal.getTimeZone().toZoneId());O padrão é unidirecional: legado → java.time é simples; para qualquer coisa nova, permaneça em java.time e converta apenas no limite da API onde vive o código antigo. Os capítulos Legacy Date e Calendar no final desta parte cobrem a ponte em detalhes.
Um exemplo completo: a família de tipos em um programa
O programa abaixo usa todos os tipos que o mapa acima introduziu — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period — e mostra como eles se convertem entre si. É a versão "tour"; cada tipo individual tem seu próprio capítulo a partir daqui.
O que observar na execução:
- O primeiro bloco construiu a família por composição:
LocalDate+LocalTime=LocalDateTime;LocalDateTime+ZoneId=ZonedDateTime;ZonedDateTime→Instant. Essa é a rede de conversões, e você fará isso toda vez que cruzar um limite de API. As setas vão nos dois sentidos para a maioria dos pares —Instant.atZone(zone)eZonedDateTime.toLocalDateTime()fecham os ciclos. - Um único
Instantexibiu três horas de aparência diferente quando visto de Nova York, Berlim e Tóquio. Esse é o ponto deInstant: é o momento, independente de onde você está. OZonedDateTimeadiciona o rótulo "onde estou." Confundir os dois é o erro doDatelegado. Durationimprimiu comoPT1H30MePeriodimprimiu comoP3M. O formato de duração ISO-8601 éPnYnMnDTnHnMnS— tudo antes doTsão unidades de calendário (Period), tudo depois são unidades de tempo (Duration). A string é exatamente o quetoString()retorna e exatamente o queparse(...)aceita.today.plusDays(7)produziu umLocalDatediferente. Imprimirtodaynovamente logo depois mostrou que o original não foi alterado — essa é a garantia de imutabilidade. Todoplus/minus/withretorna um novo objeto; o receptor nunca é modificado. Sem cópia defensiva, sem preocupações de thread-safety, nunca.ChronoUnit.DAYS.between(today, launch)foi a operação de "distância." Ela retorna umlong, não umPeriod, porque a resposta em dias não tem ambiguidade de calendário (ao contrário de meses, que variam em comprimento). Cada capítulo nesta parte usaChronoUnitem algum lugar — é o catálogo de unidades de tempo sobre as quais a API fala.
O que vem a seguir
O próximo capítulo, Java LocalDate, inicia o tour em profundidade. LocalDate é o mais simples dos cinco tipos de "ponto no tempo" e o lugar certo para aprender a forma fluente que todos os outros compartilham.