Introducción al API de fechas y tiempo de Java

La gestión del tiempo en Java ha sufrido un gran rediseño en la versión 8 de Java y posteriores, llevándola un paso más allá hacia un Api bien estructurado y muy potente. En este artículo explicamos cómo usarlo y en qué se difiere del api previo a java.time.

Introducción a la gestión de fechas con Java

Actualmente en el Jdk de Java hay dos Apis para la gestión de calendarios, fechas y horas que puede usar el programador. El primero de ellos es un api obsoleto que será eliminado en un futuro de Java, y corresponde con el uso de java.util.Date, java.util.Calendar y relacionados. Estos objetos están prácticamente desde el inicio de Java y a través de la evolución del lenguaje y su uso en entornos cada vez más diversos se han quedado desfasados ya que no se tuvieron en cuenta algunos aspectos en la fase de diseño, como por ejemplo la concurrencia y la inmutabilidad.

Para remediar los puntos negativos, en la versión de Jdk 8 y posteriores, el lenguaje incorpora un nuevo api para la gestión de tiempo, denominado de forma común por java.time por el paquete en el que están ubicadas las clases que lo conforman. Veremos en las siguientes líneas cómo usar cada uno.

Gestión de tiempos y fechas en Jdk 8+

Recuerda que el API de tiempos sólo está disponible en Java 8 y superiores. Si trabajas con una versión anterior conviene que revises la siguiente sección.

El api conocido como java.time corresponde a un rediseño por completo, maduro y estable por parte de la comunidad de mantenedores de Java, en el que se han incluido las mejores técnicas para el control de tiempo permitiendo que Java pueda ser usado en entornos tan diversos y complejos como el cálculo de fechas astronómicas con una precisión asombrosa. Además se ha tenido en cuenta la facilidad, la inmutabilidad y la concurrencia para evitar problemas en el uso o en la ejecución de aplicaciones complejas.

Los principales objetos de java.time para la gestión de horas y fechas son:

El uso de cualquiera de ellas es bastante intuitivo puesto que aportan métodos estáticos para obtener nuevas instancias, y métodos que permiten operar con ellos, siempre generando una nueva instancia gracias al concepto de inmutabilidad. Veamos algunos ejemplos:

import java.time.*;

LocalDate localDate1 = LocalDate.now();
LocalDate localDate2  = LocalDate.of(2020, 02, 20);
LocalDate localDate3 = LocalDate.parse("2020-02-20");

DayOfWeek dow = localDate1.getDayOfWeek();
int dom = localDate1.getDayOfMonth();

LocalDate localDate4 = localDate1.plusDays(3);
...

    

Los métodos de consulta y operación están fuertemente unificados entre los diferentes objetos del paquete, así que será tremendamente fácil incorporar y operar con cualquiera de ellos. Realizar tareas que con el api obsoleto resultaban arduas, con el nuevo se reducen a algunas pocas líneas, ganando en confianza y reduciendo errores.

Por último, el nuevo Api incorpora un nuevo formatter, esta vez sí reutilizable y thread-safe que permite re aprovechar la instancia para diferentes construcciones o formateos en paralelo. Éste nuevo formatter está ubicado en la clase java.time.format.DateTimeFormatter y su construcción sigue la regla de los métodos estáticos:

import java.time.*;
import java.time.format.*;

DateTimeFormatter formatter = DateTimeFormatter.of("yyyy-MM-dd");

// de String a LocalDate
LocalDate ld = LocalDate.parse("2020-04-03", formatter);

// de LocalDate a String
String ld_str = ld.format(formatter);
    

Los ejemplos puestos aquí son equivalentes para los objetos como ZoneDateTime, con la salvedad de que para aquellos que conlleven la información de la Zona (latitud) a la que pertenecen será necesario aportar un id de zona u offset para que Java pueda construir una instancia correctamente.

Fechas y horas Jdk pre 8

Recuerda que este es un API obsoleto y que será eliminado. Sólo deberías usar este si debes mantener alguna aplicación existente en el que sea impracticable el cambio del API viejo por el nuevo.

La gestión de tiempo en Jdk pre 8 consiste principalemente en el uso del objeto java.util.Date. Éste se basa en el almacenamiento y gestión de un determinado instante en base a los milisegundos transcurridos desde 1970. Por tanto, cuando se crea una fecha según la hora del sistema, o cuando se realiza de forma manual indicando día, mes, año y hora, éste convierte el conjunto de estos números a milisegundos. Por tanto, puede deducirse que será un problema la suma de fechas u horas que estén asociadas a diferentes latitudes.

Otro de los problemas es que estos objetos no son inmutables, por lo que puede haber referencias a la misma instancia desde diferentes puntos de la aplicación. Si uno de ellos modifica una fecha, para el otro consumidor la fecha también se habrá modificado. Este comportamiento no es deseable, sobre todo en entornos concurrentes con varios hilos en ejecución ya que da problemas en la depuración y correción de errores.

En cualquier caso, siendo conscientes de las limitaciones que tiene, el uso de java.util.Date está muy extendido en muchas aplicaciones por el amplio tiempo que ha estado desplegado este api en las diferentes versiones hasta que apareció Java 8. Por este motivo puede ser necesario tener que usar alguna de estas clases, veamos algunos ejemplos:

import java.util.Date;
import java.text.SimpleDateFormat;
...

// fecha del sistema con hora, minutos y segundos
Date fecha1 = new Date();

// fecha manual
Date fecha2 = new Date(2020,01,01,12,0,0);

// convertir fecha a string
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
String fecha1_str = sdf.format(fecha1);

// convertir string a fecha
Date fecha3 = sdf.parse("31/12/2020");

    

Mediante el objeto Date podemos manipular la fecha almacenada, como cambiar los minutos o los segundos. Por otro lado la clase java.text.SimpleDateFormat permite establecer un patrón en base al uso de uno símbolos específicos para las fechas con el que convertir de o hacia una cadena de caracteres. El patrón usado indica:

Hay muchos más símbolos para fechas y horas que se pueden usar, sobre todo para horas, minutos, zonas horarias, horario de verano, etc.... Conviene revisarlos porque los símbolos se mantienen en el nuevo api de fechas.

Aunque el objeto Date es mutable, para alterar una fecha se recomienda pasar por otra clase que hace una gestión más completa en el ajustes de horas y tiempo, por ejemplo para tener presente el horario de verano o invierno. Esta se llama java.util.Calendar y su extensión correspondiente al calendario común más usado java.util.GregorianCalendar. Es preferible hacer modificaciones de fechas usando estos para evitar posibles problemas por el uso puro de los milisegundos que almacena Date. Veamos unos ejemplos:

import java.text.SimpleDateFormat;
import java.util.Calendar;
...

Date date =  new Date();

Calendar calendar = Calendar.getInstance();
calendar.setTime(date);

// sumar tres meses
calendar.add(Calendar.MONTH, 3);

// añadir 25 horas
calendar.roll(Calendar.HOUR, 25);

Date dateMod = calendar.getTime();
    

Como puede verse en el ejemplo dado un Date, se consigue otro objeto del mismo tipo habiéndolo modificado mediante el uso intermedio del Calendar. Éste tiene varios métodos con los que poder operar con la fecha almacenada en su interior, dejamos la referencia al api de Calendar para más información.

Convertir LocalDates a Dates y viceversa

Cuando se diseñó el Api de Java 8 ya se planteó la opción de que las aplicaciones pudieran convivir usando ambos Apis, así que modificaron ligeramente los objetos de ambas bandas para poder hacer la conversión de unos a otros. Estos métodos desaparecerán cuando el Api obsoleto sea retirado del Jdk.

Normalmente, para convertir objetos de un api al otro se requiere pasar por una instancia de Instant como se indica en los siguientes ejemplos:

Convertir LocalDate to Date:

ZoneId defaultZoneId = ZoneId.systemDefault();
LocalDate localDate = LocalDate.of(2016, 8, 19);
Date date = Date
        .from(localDate.atStartOfDay(defaultZoneId).toInstant());
    

Convertir Date to LocalDate:

Date date = new Date();
ZoneId defaultZoneId = ZoneId.systemDefault();
Instant instant = date.toInstant();
LocalDate localDate = instant
        .atZone(defaultZoneId).toLocalDate();
    

Obviamente con ZonedDateTime el proceso será algo más sencillo, puesto que éste ya contiene información de la zona horaria sin que sea necesario hacérsela llegar de nuevo.

Conclusiones

Como puede observarse las operaciones básicas sobre tiempos y fechas en Java son bastante sencillas usando el api incorporado en el JDK 8 y que se ha mantenido en las siguientes versiones. Es la manera conveniente de gestionar este tipo de información en cualquier desarrollo y programa con Java que estés realizando o manteniendo. Se recomienda, siempre que sea posible, sustituir los usos de Date por LocalDate o LocalDateTime según corresponda, o incluso mediante ZoneDateTime si mantienes o desarrollas una aplicación global.