[Pesquisar este blog]

quarta-feira, 30 de setembro de 2015

Nova API DateTime

A API Java sempre foi bastante extensa e flexível, mas seu suporte para datas, horários e intervalos de tempo foi sempre motivo de muitas críticas. A classe java.util.Date, oferecida desde a versão 1.0 e, depois, a classe java.util.Calendar, incorporada na versão 1.1, exibem um projeto pouco intuitivo, para dizer o mínimo.

Por exemplo, na classe java.util.Date os dias do mês são iniciados em 1, mas os meses são iniciados em 0, sem contar que o ano inicial é 1900. Outra limitação é que esta API oferece apenas uma implementação de calendário, java.util.GregorianCalendar.  Além disso, muitas das classes destinadas ao uso de datas e horas não são seguras para uso com threads, aumentando as dificuldades de sua utilização em qualquer software mais sofisticado.

Por conta disso a Java 8 trouxe uma nova API DateTime, contida no pacote java.time, para a manipulação de datas e horas. A nova API é baseada na biblioteca independente JodaTime, que há vários anos constituiu a melhor alternativa disponível para estas tarefas. De fato, Stephen Colebourne, autor da API JodaTime, fez parte do time de desenvolvimento da nova API na Oracle.

A nova API DateTime é centrada em três conceitos: classes de valor imutável, projeto dirigido ao domínio data-hora e existência de calendários diferentes.

Para garantir uso adequado em aplicações multithreaded, todas classes da nova API são imutáveis, contendo apenas valores bem definidos. Qualquer alteração gera novos objetos, evitando que as ações de uma thread em um objeto provoque efeitos colaterais em outras threads que compartilham tal objeto.

O projeto da nova API reflete cuidado e rigor na definição conceitual de datas, horários e intervalos, permitindo uso mais preciso e, ao mesmo tempo, mais intuitivo. Além disso possibilita operar com sistemas de calendários diferentes, ou seja, não apenas o calendário gregoriano, comum no mundo ocidental e padronizado pela ISO8601, mas também outros, como os calendário japonês, chinês ou tailandês.

Classes principais

As principais classe do novo pacote java.time são destinadas à manipulação de datas e horários locais, de fusos horários e de intervalos de tempo. São elas:
  • Clock
  • Instant
  • LocalDate
  • LocalTime
  • LocalDateTime
  • ZonedDateTime
  • Duration

Classes java.time.Clock e java.time.Instant

A classe Clock permite obter um relógio capaz de prover acesso ao tempo (no formato UTC - Universal Time Coordinated), bem como a data e horário locais (segundo o fuso horário ajustado no sistema).

O método estático systemUTC() da classe Clock é uma fábrica que permite obter um objeto do tipo Clock. Com este objeto podem ser usados os métodos instant(), que fornece uma instância de java.time.Instant, a qual modela um instante UTC e cuja representação textual segue o formato ISO-8601; e millis(), que retorna a contagem de milisegundos UTC (a partir da 01/01/1970). O uso destas classes é bastante direto, como mostra o exemplo que segue, que também inclui a obtenção do instante UTC por meio do método estático currentTimeMillis() da classe System e de instância da tradicional classe Date.

/* Arquivo: TesteClock.java
 */
package jandl.j8d;

import java.time.Clock;
import java.time.Instant;
import java.util.Date;

public class TesteClock {

    public static void main(String[] args) {
        // Obtém relógio
        final Clock clock = Clock.systemUTC();
        // Exibição de Clock
        System.out.println("Clock   : " + clock.instant());
        System.out.println("Clock   : " + clock.millis());
        // Obtém instante atual como UTC usando Clock
        Instant agora = clock.instant();
        System.out.println("Instante: " + agora);
        System.out.println("Instante: " + agora.toEpochMilli());
        // Obtém instante atual como UTC usando System e Date
        System.out.println("System  : " + 
                System.currentTimeMillis());
        Date data = new Date();
        System.out.println("Date    : " + data);
        System.out.println("Date    : " + data.getTime());
         
        // Pausa de 10 segundos
        try {
             Thread.sleep(10000);
        } catch (InterruptedException ie) {}
        System.out.println();
         
        // Reexibição de Clock, Instant, System e Date
        System.out.println("Clock   : " + clock.instant());
        System.out.println("Clock   : " + clock.millis());
        System.out.println("Instante: " + agora);
        System.out.println("Instante: " + agora.toEpochMilli());
        System.out.println("System  : " +
                System.currentTimeMillis());
        System.out.println("Date    : " + data);
        System.out.println("Date    : " + data.getTime());
    }
}

Um resultado deste exemplo é:
Clock   : 2015-09-28T15:13:32.529Z
Clock   : 1443453212622
Instante: 2015-09-28T15:13:32.622Z
Instante: 1443453212622
System  : 1443453212622
Date    : Mon Sep 28 12:13:32 BRT 2015
Date    : 1443453212623

Clock   : 2015-09-28T15:13:42.648Z
Clock   : 1443453222648
Instante: 2015-09-28T15:13:32.622Z
Instante: 1443453212622
System  : 1443453222648
Date    : Mon Sep 28 12:13:32 BRT 2015
Date    : 1443453212623

Nele pode ser observado que Clock, Instant, System e Date são equivalentes quando se considera a obtenção imediata de um instante UTC. No entanto, após uma pausa de 10 segundos, Clock e System indicam o avanço do tempo, enquanto Instant e Date exibem o mesmo valor, pois preservam a informação do instante de tempo em que foram obtidos. Neste sentido, Clock e Instant exibem uma semântica melhor do que System e Date, portanto recomenda-se seu uso.

De fato, a implementação de Clock utiliza a informação de System.currentTimeMillis(), provendo uma interface mais adequada. Também recomenda-se que referências de objetos Clock seja declaradas como final, pois não existe necessidade de sua alteração, visto representarem o acesso ao relógio do sistema. 

Para os curiosos, existe algum detalhamento sobre as diferenças entre os formatos Universal Time Coordinated (UTC), Universal Time (UT) e Greenwich Mean Time (GMT) na introdução da classe java.util.Date.

Classes java.time.LocalDate, java.time.LocalTime e java.time.LocalDateTime

A classe LocalDate retém apenas a informação relativa a uma data, sem conteúdo de horário, conforme o calendário definido pela ISO8601. O papel de LocalTime é o inverso, ou seja, retém apenas a informação de horário, com precisão de nanosegundos, sem os componentes da data, também no padrão ISO8601. A classe LocalDateTime contém os componentes de data e horário deste padrão.

Tanto a classe LocalDate como LocalTime possuem os métodos estáticos now() e now(Clock) que retornam uma instância do respectivo tipo, contendo a data ou o horário local respectivamente. A diferença entre os objetos retornados por now() e now(Clock) é que o primeiro retorna a data/horário local segundo o fuso horário e o segundo retorna a data/horário no local segundo o relógio fornecido, mas sem considerar o fuso horário (ou seja, o horário GMT). Após sua obtenção, mesmo depois de transcorrido um intervalo de tempo qualquer, a informação contida nestas instâncias não se altera, pois preservam o instante em que foram obtidas.

O exemplo que segue mostra a obtenção de instâncias de LocalDate e LocalTime por meio dos métodos estáticos now() e now(Clock).

/* Arquivo: TesteLocalDateLocalTime.java
 */
package jandl.j8d;

import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalTime;

public class TesteLocalDateLocalTime{
    public static void main(String[] a) {
        // Obtém relógio
        final Clock clock = Clock.systemUTC();

        // Obtém data atual local
        final LocalDate dataLocal = LocalDate.now();
        final LocalDate dataLocalClock = LocalDate.now(clock);
        System.out.println("Data   : " + dataLocal);
        System.out.println("Data   : " + dataLocalClock);

        // Obtém horário atual local
        final LocalTime horarioLocal = LocalTime.now();
        final LocalTime horarioLocalClock = LocalTime.now(clock);
        System.out.println("Horário: " + horarioLocal);
        System.out.println("Horário: " + horarioLocalClock);
         
        // Pausa de 2 minutos
        try {
             Thread.sleep(120000);
        } catch (InterruptedException ie) {}
         
        // Reexibição de data e horário local
        System.out.println();
        System.out.println("Data   : " + dataLocal);
        System.out.println("Data   : " + dataLocalClock);
        System.out.println("Horário: " + horarioLocal);
        System.out.println("Horário: " + horarioLocalClock);
    }
}

Os resultados obtidos permitem comprovar que, mesmo após um intervalo de tempo, os valores armazenados não se alteram. Observe ainda que o horário local é apresentado com ou sem o fuso horário. Da mesma maneira, a data poderia ser diferente conforme o horário em relação ao fuso.
Data   : 2015-09-28
Data   : 2015-09-28
Horário: 13:47:30.532
Horário: 16:47:30.532

Data   : 2015-09-28
Data   : 2015-09-28
Horário: 13:47:30.532
Horário: 16:47:30.532

A aplicação da classe java.time.LocalDateTime segue a mesma estratégia. O exemplo que segue mostra sua aplicação direta.

/* Arquivo: TesteLocalDateTime.java
 */
package jandl.j8d;

import java.time.Clock;
import java.time.LocalDateTime;

public class TesteLocalDateTime {
    public static void main(String[] a) {
        // Obtém relógio
        final Clock clock = Clock.systemUTC();

        // Obtém data e horário atual local
        final LocalDateTime dataHoraLocal = LocalDateTime.now();
        final LocalDateTime dataHoraLocalClock =
                LocalDateTime.now(clock);
        System.out.println("DataHora: " + dataHoraLocal);
        System.out.println("DataHora: " + dataHoraLocalClock);
    }
}

A execução deste exemplo mostrará como resultado a data e horário local,  apresentado com ou sem o fuso horário em função do uso dos métodos now() e now(Clock) comentados anteriormente.
DataHora: 2015-09-28T14:59:18.098
DataHora: 2015-09-28T17:59:18.098

É possível definir uma data por meio do método estático of(int, int, int) que a partir de inteiros representando ano, mês e dia do mês retorna um objeto LocalDate:

final LocalDate data = LocalDate.of(1995, 6, 20); 

Também é possível definir um horário por meio do método estático of(int, int, int) que a partir de inteiros representando horas, minutos e segundos de um dia retorna um objeto LocalTime:

final LocalTime almoco = LocalTime.of(12, 30, 00); 

Analogamente a classe LocalDateTime possui um método estático of(int, int, int, int, int, int) que toma ano, mês, dia do mês, horas, minutos e segundos de um dia produzindo um objeto LocalDateTime:

final LocalDateTime almocoDeHoje =
        LocalDateTime.of(2015, 09, 30, 12, 30, 00);

A classe LocalDate possui o método atTime(LocalTime) que permite combinar um objeto LocalDate contendo data com outro LocalTime contendo um horário retornando um objeto LocalDateTime equivalente com as duas informações. A classe LocalTime possui o método atDate(LocalDate) que analogamente permite combinar um objeto LocalDate contendo uma data e retornando um objeto LocalDateTime equivalente.

O exemplo que segue mostra o uso direto dos métodos fábrica of das classes LocalDate, LocalTime e LocalDateTime, além da combinação de objetos LocalDate e LocalTime para produzir objetos LocalDateTime.

/* Arquivo: TesteFabricasDateTime.java
 */
package jandl.j8d;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class TesteFabricasDateTime {
    public static void main(String[] args) {
        // Data criada com método fábrica
        final LocalDate republica = LocalDate.of(2015, 11, 15);
        System.out.println(republica);

        // Horário criado com método fábrica
        final LocalTime oitoDaNoite = LocalTime.of(20, 00, 00);
        System.out.println(oitoDaNoite);
         
        // Combinação de data+hora
        final LocalDateTime ldt1 = republica.atTime(oitoDaNoite);
        final LocalDateTime ldt2 = oitoDaNoite.atDate(republica);
        System.out.println(ldt1 + " = " + ldt2);

        // Data Horário criado com método fábrica
        final LocalDateTime natal =
                 LocalDateTime.of(2015, 12, 25, 00, 00, 00);
         System.out.println(natal);
    }
}

Este programa produz os seguintes resultados:
2015-11-15
20:00
2015-11-15T20:00 = 2015-11-15T20:00
2015-12-25T00:00

Todos estes objetos são imutáveis e não refletirão a passagem do tempo, pois retêm a informação do instante em que foram obtidos. Ao mesmo tempo, podem ser manipulados para obter-se datas e horários anteriores e posteriores, situação em que novos objetos são retornados, garantindo a imutabilidade de todos.

As classes LocalDate, LocalTime e LocalDateTime também possuem os seguintes métodos que permitem adicionar ou retirar quantidades de tempo expressas de diferentes maneiras, retornando novos objetos que refletem datas e horários anteriores e posteriores no tempo:

MétodoLocalDateLocalTimeLocalDateTime
LocalDate minus(TemporalAmount)Sim

LocalTime minus(TemporalAmount)
Sim
LocalDateTime minus(TemporalAmount)

Sim
LocalDate minusYears(long)
LocalDate minusMonths(long)
LocalDate minusWeeks(long)
LocalDate minusDays(long)
Sim

LocalDateTime minusYears(long)
LocalDateTime minusMonths(long)
LocalDateTime minusWeeks(long)
LocalDateTime minusDays(long)


Sim
LocalTime minusHours(long)
LocalTime minusMinutes(long)
LocalTime minusSeconds(long)
LocalTime minusNanos(long)

Sim
LocalDateTime minusHours(long)
LocalDateTime minusMinutes(long)
LocalDateTime minusSeconds(long)
LocalDateTime minusNanos(long)


Sim
LocalDate plus(TemporalAmount)Sim

LocalTime plus(TemporalAmount)
Sim
LocalDateTime plus(TemporalAmount)

Sim
LocalDate plusYears(long)
LocalDate plusMonths(long)
LocalDate plusWeeks(long)
LocalDate plusDays(long)
Sim

LocalDateTime plusYears(long)
LocalDateTime plusMonths(long)
LocalDateTime plusWeeks(long)
LocalDateTime plusDays(long)


Sim
LocalTime plusHours(long)
LocalTime plusMinutes(long)
LocalTime plusSeconds(long)
LocalTime plusNanos(long)

Sim
LocalDateTime plusHours(long)
LocalDateTime plusMinutes(long)
LocalDateTime plusSeconds(long)
LocalDateTime plusNanos(long)


Sim

Um exemplo simples do uso destes métodos está incluído mais à frente junto a apresentação da classe java.time.Duration.

Classe java.time.ZonedDateTime

Nas situação onde se deseja a data e o horário para uma localização específica, isto é, pertencente a outro fuso horário, deve ser utilizada a classe ZonedDateTime

Como antes, a classe ZonedDateTime possui os métodos estáticos now() e now(Clock) que retornam uma instância do respectivo tipo, contendo a data e o horário local. Adicionalmente possui o método estático now(ZoneId) que permite indicar o fuso horário desejado. Após sua obtenção, mesmo depois de transcorrido um intervalo de tempo qualquer, a informação contida nestas instâncias não se altera, pois preservam o instante em que foram obtidas.

O exemplo que segue mostra a obtenção de instâncias de ZonedDateTime por meio dos métodos estáticos now(), now(Clock) e now(ZoneId).

/* Arquivo: TesteZonedDateTime.java
 */
package jandl.j8d;

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

public class TesteZonedDateTime {
    public static void main(String[] args) {
        // Obtém relógio
        final Clock clock = Clock.systemUTC();
        // Obtém data e horário conforme fusos específicos
        final ZonedDateTime zonedDatetime = ZonedDateTime.now();
        final ZonedDateTime zonedDatetimeFromClock =
                ZonedDateTime.now(clock);
        final ZonedDateTime zonedDatetimeFromZone1 =
                ZonedDateTime.now(
                        ZoneId.of("America/New_York"));
        final ZonedDateTime zonedDatetimeFromZone2 =
                ZonedDateTime.now(
                        ZoneId.of("Europe/Lisbon"));
        // Exibe data e horário conforme fusos específicos
        System.out.println(zonedDatetime);
        System.out.println(zonedDatetimeFromClock);
        System.out.println(zonedDatetimeFromZone1);
        System.out.println(zonedDatetimeFromZone2);
    }
}

O resultado deste programa exibe a data, horário e o fuso horário default e das regiões indicadas. 
2015-09-28T15:07:45.540-03:00[America/Sao_Paulo]
2015-09-28T18:07:45.540Z
2015-09-28T14:07:45.543-04:00[America/New_York]
2015-09-28T19:07:45.555+01:00[Europe/Lisbon]

A classe LocalDateTime possui o método atZone(ZoneId) que permite combinar um objeto contendo data-horário com um fuso horário retornando um objeto ZonedDateTime equivalente.

Classe java.time.Duration

A representação de intervalos de tempo pode ser feita com uso da classe Duration, que internamente armazena uma contagem de segundos e nanosegundos. Conforme a documentação oficial, a duração do intervalo é armazenada num campo long, ou seja, permite indicar intervalos maiores do que a idade estimada do universo. Para alcançar a precisão em nanosegundo, também contém uma porção int para armazenar os nanosegundos do segundo (um valor entre 0 e 999.999.999). As durações podem ser positivas ou negativas.

Esta classe também permite manipular intervalos de tempo, facilitando e muito sua utilização. Existem métodos que podem diminuir/reduzir o intervalo:
  • Duration minus(Duration),
  • Duration minusDays(long), 
  • Duration minusHours(long),
  • Duration minusSeconds(long),
  • Duration minusMillis(long)
  • Duration minusNanos(long).
Bem como métodos para aumentar/ampliar o intervalo:
  • Duration plus(Duration)
  • Duration plusDays(long),
  • Duration plusHours(long),
  • Duration plusSeconds(long), 
  • Duration plusMillis(long)
  • Duration plusNanos(long).

O exemplo que segue define datas inicio e fim diretamente com LocalDateTime.of(int, int, int, int, int, int, int) e, a partir destas datas, obtém um intervalo de tempo positivo (fim-inicio), manipula da data fim para obter uma anterior (fimMenos10), obtém um intervalo negativo (inicio-fimMenos10) e finalmente manipula um intervalo obtendo um outro maior. Várias informações dos objetos obtidos são exibidas ao longo do programa.

/* Arquivo: TesteDuration.java
 */
package jandl.j8d;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Month;

public class TesteDuration {

    public static void main(String[] args) {
        // Define duas datas-horário, início e fim
        final LocalDateTime inicio = 
            LocalDateTime.of(2002, Month.FEBRUARY, 20, 0, 0, 0);
        final LocalDateTime fim = 
            LocalDateTime.of(2015, Month.JULY, 31, 23, 59, 59);

        // Determina intervalo entre datas fim-inicio
        // (duração positiva)
        final Duration duracaoIF = Duration.between(inicio, fim);
        // Exibe intervalo e seus detalhes
        System.out.printf("Duracao [%s - %s] = %s\n",
                fim, inicio, duracaoIF);
        System.out.printf(
                "Dias=%0,3d  Horas=%0,3d  Min=%0,3d  Seg=%0,3d\n\n",
                duracaoIF.toDays(), duracaoIF.toHours(),
                duracaoIF.toMinutes(), duracaoIF.toMillis()/1000);

        // Define nova data por meio manipulação de data existente
        final LocalDateTime fimMenos10 = fim.minusDays(10);

        // Determina intervalo entre datas inicio-fim
        // (duração negativa)
        final Duration duracaoFI = 
                Duration.between(fimMenos10, inicio);
        // Exibe intervalo e seus detalhes
        System.out.printf("Duracao [%s - %s] = %s\n",
                inicio, fimMenos10, duracaoFI);
        System.out.printf(
                "Dias=%0,3d  Horas=%0,3d  Min=%0,3d  Seg=%0,3d\n\n",
                duracaoFI.toDays(), duracaoFI.toHours(),
                duracaoFI.toMinutes(), duracaoFI.toMillis()/1000);

        // Define um intervalo por meio de método-fábrica
        Duration dezDias = Duration.ofDays(10);
        // Novo intervalo obtido pela manipulação 
        // de intervalo existente
        Duration duracaoMais11 = 
                duracaoIF.plus(dezDias).plusDays(1);
        // Exibe novo intervalo
        System.out.printf("Duracao + 11dias = %s\n", duracaoMais11);
    }
}

Os resultados deste programa são como os que seguem:
Duracao [2015-07-31T23:59:59 - 2002-02-20T00:00] = PT117839H59M59S
Dias=4,909  Horas=117,839  Minutos=7,070,399  Segundos=424,223,999

Duracao [2002-02-20T00:00 - 2015-07-21T23:59:59] = PT-117599H-59M-59S
Dias=-4,899  Horas=-117,599  Minutos=-7,055,999  Segundos=-423,359,999

Duracao + 11dias = PT118103H59M59S

Retrocompatibilidade

Para manter a compatibilidade com aplicações desenvolvidas nas versões anteriores ao Java 8, as antigas classes destinadas à manipulação de datas e horas foram mantidas. Para facilitar a convivência com a imensa quantidade de código que utiliza a antiga API, uma pequena e elegante alteração foi feita na classe java.util.Date. Nela foi adicionado o método toInstant(), o qual converte o antigo objeto em um novo, do tipo Instant. Isso permite migrar mais facilmente o código entre a velha e a nova API.

Considerações finais

A nova API DateTime introduzida no Java 8 é um avanço substancial em relação as classes incluídas anteriormente na API. Além de serem relativamente simples de utilizar, são bastante consistentes em termos de sua semântica e também rigorosas em relação ao domínio do tempo. O fato de serem imutáveis também é um ganho, pois tornará mais simples o desenvolvimento de aplicações multithreaded.

Desta maneira, a recomendação é que o desenvolvimento de novas aplicações dê preferência ao uso das novas classes disponíveis no pacote java.time. Além disso, sempre que possível deve ser considerada a migração do código da antiga API para esta nova, pois os ganhos são efetivos. 

Referências


Este artigo faz parte de uma pequena série:


Nenhum comentário: