Le pourquoi du comment

L’une des grandes nouveautés de Java 8 à ne surtout pas louper est notamment le nouveau package permettant le traitement des Dates.
Afin de bien comprendre pourquoi notre traditionnel java.util.Date a été remplacé, il va falloir ouvrir les yeux sur les défauts qu’on lui a pardonnés pendant des années, sans vraiment oser se l’avouer.

Tout d’abord, la classe java.util.Date faisait à moitié le travail :

  • La séparation Jour / Heure n’était pas présente, ce qui posait des soucis lorsque l’on calculait le J-1 d’une date lors d’un changement d’horaire d’été/hiver,
  • La plupart des méthodes sont deprecated, on se sentait à l’étroit.

L’utilisation de la classe java.text.SimpleDateFormat devenait un véritable supplice… Deux solutions s’offraient à nous :

  • Déclarer le formateur comme variable membre static final, mais seul le vieux sage connaissait sa limitation ThreadSafe,
  • Créer le parseur lors de son utilisation, la rendant ainsi ThreadSafe, mais surchargeant inutilement le tas d’objets à supprimer.

À noter au passage que les packages ne sont pas unifiés. Un coup c’est le package java.util, un coup c’est le package java.text.
Le petit malin notera qu’il n’est pas évident de savoir que les objets de ces packages sont en relation avec la notion de date.

Vous avez certainement eu d’autres déconvenues avec ces librairies, il était temps que cela change…

La gestion des dates

Pour faire simple et court, la classe java.util.Date a été découpée en plusieurs classes en fonction des besoins :

  • java.time.LocalDate permet de gérer des dates uniquement (pas les heures) et sans gestion de fuseau horaire.
  • java.time.LocalTime permet de gérer des heures uniquement (sans date) et sans gestion de fuseau horaire.
  • java.time.LocalDateTime permet de gérer des dates avec heures et sans gestion de fuseau horaire.
  • java.time.Clock permet de gérer des dates avec heures et avec gestion de fuseau horaire.

Il y a plein d’autres classes, chacune ayant son périmètre fonctionnel : Duration, Period, Year, YearMonth, ZonedDateTime, etc…
Il serait faux de faire une correspondance entre un existant et son équivalent JDK8. Traduire Date par Clock serait trop rapide. Il serait souhaitable de prendre en considération l’utilisation réelle que l’on faisait de cette fameuse classe java.util.Date.

Le formatage des dates

Mais que devient notre java.text.SimpleDateFormat ?
Il devient java.time.format.DateTimeFormatter !
Son utilisation est légèrement différente.
Les différents objets représentant une date possèdent une fonction de parsing et de formatage. Le formateur à utiliser doit être fournit en argument de la fonction et non l’inverse.
Mais surtout, il devient ThreadSafe.

Avant

SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
Date now = new Date();
LOGGER.info("Il est maintenant : " + sdf.format(now));

 Après

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
LocalDateTime nowDate = LocalDateTime.now();
LOGGER.info("Il est maintenant : " + nowDate.format(formatter));

 Remarque intéressante (çà m’arrive !)

Lorsqu’on utilise une formatter comportant heure et minute dans un objet de type java.time.LocalDate, voici la belle erreur qui nous est remontée :

Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay
at java.time.LocalDate.get0(Unknown Source)
at java.time.LocalDate.getLong(Unknown Source)
at java.time.format.DateTimePrintContext.getValue(Unknown Source)
at java.time.format.DateTimeFormatterBuilder$NumberPrinterParser.format(Unknown Source)
at java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.format(Unknown Source)
at java.time.format.DateTimeFormatter.formatTo(Unknown Source)
at java.time.format.DateTimeFormatter.format(Unknown Source)
at java.time.LocalDate.format(Unknown Source)
at DateTest.simpleDateFormat(DateTest.java:24)

Et notre Calendar dans tout çà ?

On avait pris l’habitude d’écrire des lignes et des lignes de code pour ajouter 3 jours à une date, retrancher 2 heures à notre timing, mais sans réellement savoir s’il fallait faire un roll, un add ou encore autre chose….
Bref, c’est désormais réglé, c’est simple, c’est inclus dans chacun des objets de Date.
Attention quand même à un détail : Les classes sont immuables, il faut donc veiller à récupérer la nouvelle instance créée lorsqu’une opération est effectuée. Les instances de ces classes ne sont pas modifiables.

Avant

SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
Date oldDate = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(oldDate);
calendar.roll(Calendar.DAY_OF_YEAR, -1);
oldDate = calendar.getTime();
LOGGER.info("Il est maintenant : " + sdf.format(oldDate));

 Après

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
LocalDateTime newDate = LocalDateTime.now();
newDate = newDate.minusDays(1);
LOGGER.info("Il est maintenant : " + newDate.format(formatter));

Trucs & Astuce

La migration des librairies externes risque de prendre du temps…
Il existera donc une phase de transition où devront cohabiter ancien et nouveau.

Voici quelques formules de conversion :

LocalDateTime newLocalDateTime = LocalDateTime.ofInstant(oldDate.toInstant(), ZoneId.systemDefault());
LocalDateTime newLocalDateTime = LocalDateTime.ofInstant(oldCalendar.toInstant(), ZoneId.systemDefault());

Section « Je ronchonne »

La différence entre LocalDateTime et Instant est trop mineure. On pourra retenir que si deux instances de Instant sont créées au même moment à deux endroits de la terre, les valeurs seront identiques car la référence est le 1 janvier 1970 en UTC. L’objet LocalDateTime se rapproche plus de l’horloge murale que l’on a tous dans notre bureau.

Je n’ai pas de fonction plusMillis(long millisToAdd) dans la classe LocalDateTime alors qu’elle est bien présente dans la classe Instant.

Ces classes sont immuables (aucune propriété ne change après sa création), chaque opération retourne une copie de classe. C’est fabuleux pour faire de jolies expressions inline et cela les rend Thread-Safe par définition. MAIS… à vous de compter le nombre d’instances créées inutilement dans mon petit exemple ci dessous…

LocalDateTime resultNextDateTime = resultNextDateTime.withHour(taskPeriod.getOffsetHour()).withMinute(taskPeriod.getOffsetMinute()).withSecond(taskPeriod.getOffsetSecond());

En conclusion

C’est quand même beaucoup mieux non ?
Ce n’était pas trop tôt !
La manipulation et le formatage sont maintenant des fonctions inhérentes aux dates… Logique me direz-vous…

L’API JSR-310 permet de régir le tout.

A vos dates 😉