Conheça a nova API de datas do Java 8

Manipular datas no Java sempre foi algo trabalhoso. No Java 1.0 havia apenas a classe Date, que era complicada de usar e não funcionava bem com internacionalização. Com o lançamento do Java 1.1, surgiu a classe abstrata Calendar, com muito mais recursos, porém com mutabilidade e decisões de design questionáveis.

A partir do Java 8, que foi lançado recentemente, há uma nova API de datas disponível no pacote java.time. Essa API é uma excelente adição às bibliotecas padrão do Java e já vinha sendo desenvolvida desde 2007.

Essa nova API foi baseada na famosa biblioteca JodaTime, que era a salvação para lidar com datas até então. Já falamos sobre como usar o JodaTime para resolver problemas difíceis. A nova API não é exatamente igual ao JodaTime, já que vários detalhes conceituais e de implementação foram melhorados.

Um dos principais conceitos dessa nova API é a separação de como dados temporais são interpretados em duas categorias: a dos computadores e a dos humanos.

Datas para computadores

Para um computador, o tempo é um número que cresce a cada instante. No Java, historicamente era utilizado um long que representava os milissegundos desde 01/01/1970 às 00:00:00. Na nova API, a classe Instant é utilizada para representar esse número, agora com precisão de nanossegundos.

Instant agora = Instant.now();
System.out.println(agora); //2014-04-08T10:02:52.036Z (formato ISO-8601)

Podemos usar um Instant, por exemplo, para medir o tempo de execução de um algoritmo.

Instant inicio = Instant.now();
rodaAlgoritmo();
Instant fim = Instant.now();

Duration duracao = Duration.between(inicio, fim);
long duracaoEmMilissegundos = duracao.toMillis();

Observe que utilizamos a classe Duration. Essa classe serve para medir uma quantidade de tempo em termos de nanossegundos. Você pode obter essa quantidade de tempo em diversas unidades chamando métodos como toNanos, toMillis, getSeconds, etc.

Datas para humanos

Já para um humano, há uma divisão do tempo em anos, meses, dias, semanas, horas, minutos, segundos e por aí vai. Temos ainda fusos horários, horário de verão e diferentes calendários.

Várias questões surgem ao considerarmos a interpretação humana do tempo. Por exemplo, no calendário judaico, um ano pode ter 13 meses. As classes do pacote java.time permitem que essas interpretações do tempo sejam definidas e manipuladas de forma precisa, ao contrário do que acontecia ao usarmos Date ou Calendar.

Temos, por exemplo, a classe LocalDate que representa uma data, ou seja, um período de 24 horas com dia, mês e ano definidos.

LocalDate hoje = LocalDate.now();
System.out.println(hoje); //2014-04-08 (formato ISO-8601)

Um LocalDate serve para representarmos, por exemplo, a data de emissão do nosso RG, em que não nos importa as horas ou minutos, mas o dia todo. Podemos criar um LocalDate para uma data específica utilizando o método of:

LocalDate emissaoRG = LocalDate.of(2000, 1, 15);

Note que utilizamos o valor 1 para representar o mês de Janeiro. Poderíamos ter utilizado o enum Month com o valor JANUARY. Há ainda o enum DayOfWeek, que representa os dias da semana.

Para calcularmos a duração entre dois LocalDate, devemos utilizar um Period, que já trata anos bissextos e outros detalhes.

LocalDate homemNoEspaco = LocalDate.of(1961, Month.APRIL, 12);
LocalDate homemNaLua = LocalDate.of(1969, Month.MAY, 25);

Period periodo = Period.between(homemNoEspaco, homemNaLua);

System.out.printf("%s anos, %s mês e %s dias", 
	periodo.getYears() , periodo.getMonths(), periodo.getDays());
	//8 anos, 1 mês e 13 dias

Já a classe LocalTime serve para representar apenas um horário, sem data específica. Podemos, por exemplo, usá-la para representar o horário de entrada no trabalho.

LocalTime horarioDeEntrada = LocalTime.of(9, 0);
System.out.println(horarioDeEntrada); //09:00

A classe LocalDateTime serve para representar uma data e hora específicas. Podemos representar uma data e hora de uma prova importante ou de uma audiência em um tribunal.

LocalDateTime agora = LocalDateTime.now();
LocalDateTime aberturaDaCopa = LocalDateTime.of(2014, Month.JUNE, 12, 17, 0);
System.out.println(aberturaDaCopa); //2014-06-12T17:00 (formato ISO-8601)

Datas com fuso horário

Para representarmos uma data e hora em um fuso horário específico, devemos utilizar a classe ZonedDateTime.

ZoneId fusoHorarioDeSaoPaulo = ZoneId.of("America/Sao_Paulo");
ZonedDateTime agoraEmSaoPaulo = ZonedDateTime.now(fusoHorarioDeSaoPaulo);
System.out.println(agoraEmSaoPaulo); //2014-04-08T10:02:57.838-03:00[America/Sao_Paulo]

Com um ZonedDateTime, podemos representar, por exemplo, a data de um voo.

ZoneId fusoHorarioDeSaoPaulo = ZoneId.of("America/Sao_Paulo");
ZoneId fusoHorarioDeNovaYork = ZoneId.of("America/New_York");

LocalDateTime saidaDeSaoPauloSemFusoHorario = 
	LocalDateTime.of(2014, Month.APRIL, 4, 22, 30);
LocalDateTime chegadaEmNovaYorkSemFusoHorario =
	LocalDateTime.of(2014, Month.APRIL, 5, 7, 10);
		
ZonedDateTime saidaDeSaoPauloComFusoHorario = 
	ZonedDateTime.of(saidaDeSaoPauloSemFusoHorario, fusoHorarioDeSaoPaulo);
System.out.println(saidaDeSaoPauloComFusoHorario); //2014-04-04T22:30-03:00[America/Sao_Paulo]

ZonedDateTime chegadaEmNovaYorkComFusoHorario = 
	ZonedDateTime.of(chegadaEmNovaYorkSemFusoHorario, fusoHorarioDeNovaYork);
System.out.println(chegadaEmNovaYorkComFusoHorario); //2014-04-05T07:10-04:00[America/New_York]
	
Duration duracaoDoVoo = 
	Duration.between(saidaDeSaoPauloComFusoHorario, chegadaEmNovaYorkComFusoHorario);
System.out.println(duracaoDoVoo); //PT9H40M

Se calcularmos de maneira ingênua a duração do voo, teríamos 8:40. Porém, como há uma diferença entre os fusos horários de São Paulo e Nova York, a duração correta é 9:40. Repare que a API já faz o tratamento de fusos horários distintos.

Outro cuidado importante que devemos ter é em relação ao horário de verão. No fim do horário de verão, por exemplo, a mesma hora existe duas vezes!

ZoneId fusoHorarioDeSaoPaulo = ZoneId.of("America/Sao_Paulo");

LocalDateTime fimDoHorarioDeVerao2013SemFusoHorario = 
	LocalDateTime.of(2014, Month.FEBRUARY, 15, 23, 00);

ZonedDateTime fimDoHorarioVerao2013ComFusoHorario = 
	fimDoHorarioDeVerao2013SemFusoHorario.atZone(fusoHorarioDeSaoPaulo);
System.out.println(fimDoHorarioVerao2013ComFusoHorario); //2014-02-15T23:00-02:00[America/Sao_Paulo]

ZonedDateTime maisUmaHora = 
	fimDoHorarioVerao2013ComFusoHorario.plusHours(1);
System.out.println(maisUmaHora); //2014-02-15T23:00-03:00[America/Sao_Paulo]

Repare no código anterior que, mesmo aumentando uma hora, o horário continuou 23:00. Entretanto, observe que o fuso horário foi alterado de -02:00 para -03:00.

Datas e meses importantes

Existem também as classes MonthDay, que deve ser utilizada para representar datas importantes que se repetem todos os anos, e YearMonth, que deve ser utilizada para representar um mês inteiro de um ano específico.

MonthDay natal = MonthDay.of(Month.DECEMBER, 25);
YearMonth copaDoMundo2014 = YearMonth.of(2014, Month.JUNE);

Formatando datas

O toString padrão das classes da API utiliza o formato ISO-8601. Se quisermos definir o formato de apresentação da data, devemos utilizar o método format, passando um DateTimeFormatter.

LocalDate hoje = LocalDate.now();
DateTimeFormatter formatador = 
  DateTimeFormatter.ofPattern("dd/MM/yyyy");
hoje.format(formatador); //08/04/2014

O enum FormatStyle possui alguns formatos pré-definidos, que podem ser combinados com um Locale.

LocalDateTime agora = LocalDateTime.now();
DateTimeFormatter formatador = DateTimeFormatter
	.ofLocalizedDateTime(FormatStyle.SHORT)
	.withLocale(new Locale("pt", "br"));
agora.format(formatador); //08/04/14 10:02

Manipulando datas

Todas as classes mencionadas possuem diversos métodos que permitem manipular as medidas de tempo. Por exemplo, podemos usar o método plusDays da classe LocalDate para aumentarmos um dia:

LocalDate hoje = LocalDate.now();
LocalDate amanha = hoje.plusDays(1);

Outro cálculo interessante é o número de medidas de tempo até uma determinada data, que podemos fazer através do método until. Para descobrir o número de dias até uma data, por exemplo, devemos passar ChronoUnit.DAYS como parâmetro.

MonthDay natal = MonthDay.of(Month.DECEMBER, 25);
LocalDate natalDesseAno = natal.atYear(Year.now().getValue());
long diasAteONatal = LocalDate.now()
    .until(natalDesseAno, ChronoUnit.DAYS);

Podemos utilizar a interface TemporalAdjuster para definir diferentes maneiras de manipular as medidas de tempo. É interessante notar que essa é uma interface funcional, permitindo o uso de lambdas.

A classe auxiliar TemporalAdjusters já possui diversos métodos que agem como factories para diferentes implementações úteis de TemporalAdjuster. Podemos, por exemplo, descobrir qual é a próxima sexta-feira.

TemporalAdjuster ajustadorParaProximaSexta = TemporalAdjusters.next(DayOfWeek.FRIDAY);
LocalDate proximaSexta = LocalDate.now().with(ajustadorParaProximaSexta);

Imutabilidade e Testabilidade

Se você adicionar um dia a um LocalDate, as informações de data não serão alteradas.

LocalDate hoje = LocalDate.now(); //2014-04-08
hoje.plusDays(1);
System.out.println(hoje); //2014-04-08 (ainda é hoje, e não amanhã!)

Na verdade, qualquer método que alteraria o objeto retorna uma referência a um novo objeto com as informações alteradas.

LocalDate hoje = LocalDate.now();
LocalDate amanha = hoje.plusDays(1);
boolean mesmoObjeto = hoje == amanha; //false, já que é imutável

Isso vale para todas as classes do pacote java.time, que são imutáveis e, por isso, são thread-safe e mais fáceis de dar manutenção.

Um outro ponto importante da API é a melhor testabilidade com o uso da classe Clock.

Trabalhando com código legado

Não poderemos mudar todo o nosso código existente para trabalhar com o poder do pacote java.time de uma hora pra outra. Por isso, o Java 8 trouxe alguns pontos de interoperabilidade entre os antigos Date e Calendar e a nova API.

Calendar calendar = Calendar.getInstance();
Instant instantAPartirDoCalendar = calendar.toInstant();
Date dateAPartirDoInstant = Date.from(instantAPartirDoCalendar);
Instant instantAPartirDaDate = dateAPartirDoInstant.toInstant();		

Além disso, classe abstrata Calendar ganhou um builder, que possibilita a criação de uma instância de maneira fluente.

Calendar calendario =  
      new Calendar.Builder()  
        .setDate(2014, Calendar.APRIL, 8)  
        .setTimeOfDay(10, 2, 57)  
        .setTimeZone(TimeZone.getTimeZone("America/Sao_Paulo"))  
        .setLocale(new Locale("pt", "br"))
        .build(); 

A nova API de datas do Java 8 é bastante extensa, possuindo diversos outros recursos interessantes. Com certeza, é uma adição muito bem-vinda ao Java, que vai facilitar bastante o trabalho de manipulação de datas.

37 Comentários

  1. Israel Alencar 08/04/2014 at 11:23 #

    Muito legal, principalmente para mim que estou começando a aprender java.

  2. Pedro 08/04/2014 at 13:16 #

    Realmente, trabalhar com data nas versões anteriores do java era um parto! Excelente post, com ótimos exemplos!

  3. Cassius Vinicius 08/04/2014 at 14:41 #

    Muito bem.

    Obrigado pela dica.

  4. Henrique 08/04/2014 at 14:45 #

    Essas mudanças parecem ser bem mais fácil de usar, estou iniciando e ainda tenho dificuldades com data, vou colocar o java 8 pois agora parece ser bem mais fácil!!
    Muito bom os exemplos!

  5. Rafael Ponte 10/04/2014 at 11:18 #

    Trabalhar com datas sempre foi um desafio no mundo Java, que bom que agora temos essa nova API no Java 8. Demorou, mas chegou!

    Parabéns pelo post, muito simples e direto. Um excelente overview para quem quer conhecer as novidades e possibilidades da API de datas.

  6. Bruno Paz 10/04/2014 at 22:32 #

    Excelente artigo e muito bem explicado! Parabéns

  7. Douglas Arantes 11/04/2014 at 00:52 #

    Primeiramente, parabéns pelo novo design do Blog. O artigo ficou ótimo, um overview completo da nova API.

    Uma pergunta, alguém sabe dizer se a JPA já suporta essa JSR 31o?

  8. Alexandre Aquiles 11/04/2014 at 09:05 #

    Douglas,

    Infelizmente o JPA ainda não tem suporte a API java.time. Porém, já existe uma discussão sobre o assunto no JIRA da especificação. Deve ser lançado apenas no JavaEE 8.

    Entretanto, o JDBC 4.2 suporta conversão de LocalTime para java.sql.Time, de LocalDate para java.sql.Date e de LocalDateTime para java.sql.Timestamp, e vice-versa. Além disso, java.sql.Timestamp pode ser convertido para Instant.

  9. Douglas Arantes 11/04/2014 at 10:31 #

    Existe um Projeto no BitBucket que implementa Converters do JPA 2.1, para mapear os novos tipos da JSR 310.
    https://bitbucket.org/montanajava/jpaattributeconverters

  10. Rodrigo Ferreira 11/04/2014 at 10:36 #

    Opá @Douglas, muito interessante!
    Bom saber disso.

    Valeu!

  11. Wesley Bezerra 11/04/2014 at 11:31 #

    Maturidade faz parte da nossa vida e o Java demorou um tanto para chegar a este nível. Automatizou bastante coisa interessante! Vale aguardar JPA no JAVAEE8!

  12. Fernando Souza 14/04/2014 at 23:16 #

    Ótima introdução a nova api de data. Obrigado!

  13. Mateus 15/04/2014 at 10:31 #

    Muito legal! parabéns pelo post! Bye Bye Joda API!

  14. Diogo Souza 15/04/2014 at 11:23 #

    Cara,

    Pergunta: quando Java 8 estará maduro o suficiente para adotarmos em produção, em sua opinião?

    Hoje utilizo o Joda massivamente e achei interessante aposentar meu DateUtils (com JODA) para utilizar o Java 8.

    Um abraço e muito obrigado pelas dicas, parabéns!

  15. Priscila Vieira 15/04/2014 at 11:32 #

    Boas notícias sempre alegram nosso <3 !! 🙂 Já estava mais que na hora!!! Ótimo post!!! Excelente introdução! O java 8 fará a diferença.

  16. Rodrigo Ferreira 15/04/2014 at 12:25 #

    Oi @Diogo, como o Java 8 foi lançado há pouco tempo, os frameworks, servidores, ferramentas e IDEs ainda estão começando a adotá-lo. Acredito que teremos uma adoção mais massiva após o lançamento do Java EE 8, lá pro final de 2014 ou meados de 2015.

    Mas vale a pena já começar a estudar, e adotá-lo em pequenos projetos. Na Caelum, já estamos utilizando bastante e gostando 🙂

    Abraços!

  17. Guilherme Sjlender 15/04/2014 at 13:35 #

    Escorreu uma lagrima quando li sobre essa nova API no Java 8.
    Muito bom!

    Parabéns pelo post! 😉

  18. Diogo Souza 15/04/2014 at 13:43 #

    Muito obrigado pela resposta. Um abs !

  19. Tonis 15/04/2014 at 14:22 #

    Nossa cada dia mais difícil das demais linguagens superara esse povo da oracle

  20. Diogo Souza 15/04/2014 at 16:17 #

    Excelente! Vale ressaltar a importância de tal adição à API padrão do Java, em se considerando locais de trabalho onde APIs alternativas não podem ser usadas…

    Parabéns! 🙂

  21. Roberson Campos 15/04/2014 at 17:52 #

    Excelente artigo.
    Muito bem explicado e uma grande abordagem.

  22. Lucas Fabiano 16/04/2014 at 21:05 #

    Parabéns pelo artigo. Está muito completo e detalhado.

  23. camilo lopes 21/04/2014 at 22:23 #

    muito bom o post e bem abordado. Demorou mais chegou e com tudo. Realmente uma grande mudança no Java.

  24. Denilson Telaroli 19/09/2014 at 16:06 #

    Trabalhando com Hibernate a mutabilidade do Calendar quebra muito sistema.
    Um setDate gera um update no banco de dados.

    Tenso!!!
    Já era HORA mesmo do Java evoluir esta API.

  25. Antonio Luciano Lima da Silva 08/03/2016 at 10:31 #

    Estou desenvolvendo um projeto web usando jsf2, cdi, bean validation e tenho duas datas, uma é a data de entrada e a outra é a data de saída usando o formato de timestamp, gostaria de realizar o calculo de horas nesse intervalo das duas datas, alguém tem uma ideia usando a api do java 8, eu até consigo realizar o calculo mas fica em um formato estranho.
    Segue parte do codigo

    public void somarData() {
    //DateTime dtInicial = new DateTime(2016, 10, 20, 5, 0, 0);
    //DateTime dtFinal = new DateTime(2016, 10, 20, 7, 0, 0);
    Long resultado = (getDataEntrada().getTime() – getDataSaida().getTime());
    System.out.println(“Resultado de dataEntrada e dataSaida: “+resultado);
    }

    Quando eu chamo esse método ele trás o seguinte resultado.
    Resultado de dataEntrada e dataSaida: -217856300

    Gostatia obter o resultado formatado e correto das horas e minutos entre as duas datas

  26. Alexandre Aquiles 08/03/2016 at 13:41 #

    Antônio,

    O ideal para resolver esse tipo de dúvida pontual é o GUJ Respostas: http://respostas.guj.com.br/

    Mas já que estamos aqui, vamos lá…

    Primeiramente, conforme visto no post, para períodos com data e hora definidos, a classe correta é LocalDateTime do pacote java.time. Certifique-se que você não está usando a biblioteca JodaTime.

    Quanto ao cálculo, você tem algumas formas de implementar:

    Duration duration = Duration.between(dtInicial, dtFinal);
    long horas = duration.toHours()
    System.out.println("Horas: "+ horas);
    

    ou

    long horas = ChronoUnit.HOURS.between(dtInicial, dtFinal);
    System.out.println("Horas: "+horas);
    
  27. André Luís 07/04/2016 at 21:39 #

    Ótimo post, me ajudou muito!

    Tenho apenas uma dúvida. Como verificar se um determinado intervalo de datas (dtInicial e dtFinal) está entre outro intervalo de datas (dtInicial e dtFinal)? Desejo fazer isso para evitar o cadastro de um evento simultâneo em meu projeto. Em suma, desejo evitar o conflito de eventos.

  28. Alexandre Aquiles 08/04/2016 at 20:38 #

    André,

    O melhor lugar para esse tipo de dúvidas é o GUJ: http://guj.com.br

    Infelizmente, no pacote java.time não há uma maneira de ver intersecção entre intervalos de datas.

    Porém, o criador do java.time (e do JodaTime), Stephen Colebourne, mantém um projeto que contém algumas classes utilitárias e de extensão: https://github.com/ThreeTen/threeten-extra

    Nesse projeto threeten-extra há uma classe Interval que parece ser o que você quer:
    http://www.threeten.org/threeten-extra/apidocs/org/threeten/extra/Interval.html

  29. Krisnamourt 12/05/2016 at 16:13 #

    Finalmente o Java aprendeu a trabalhar com Data. Um viva ao JodaTime!

  30. Tec Nerd 19/09/2016 at 16:47 #

    Muito bom, obrigado por esse artigo!

  31. Otávio 14/06/2018 at 08:24 #

    Mais uma vez o Java pegando uma API boa (hora) e copiando pra pior.

Deixe uma resposta