Classe Date héritée de Java
La classe java.util.Date héritée — pourquoi java.time la remplace et comment convertir entre les deux.
java.util.Date était la classe date-heure originale de Java, présente depuis Java 1.0 en 1995. Elle est toujours dans le JDK ; le nouveau code ne devrait pas l'utiliser, mais vous la rencontrerez dans des bibliothèques, des bases de données (java.sql.Date l'étend) et dans tout code antérieur à environ 2014. Ce chapitre existe pour faire le pont avec java.time.
En résumé : Date est en interne un wrapper autour d'un long représentant des millisecondes depuis l'époque, tout comme Instant est en interne une paire (seconds, nanos). La conversion naturelle est donc Date ↔ Instant. Pour toute autre opération (année, mois, jour, arithmétique calendaire), convertissez d'abord en java.time.
Ce qu'est réellement Date
public class Date implements Cloneable, Comparable<Date>, Serializable {
private long fastTime; // milliseconds since 1970-01-01T00:00:00Z
}C'est tout l'état réel. Un Date est un point dans le temps, mesuré en millisecondes depuis l'époque Unix, en UTC. Malgré son nom, il ne contient pas de date calendaire — getYear() et ses semblables calculent à partir de la valeur en millisecondes dans le fuseau horaire par défaut de la JVM, ce qui est à l'origine des problèmes notoires de cette API.
Date now = new Date(); // current moment
Date epoch = new Date(0); // 1970-01-01T00:00:00Z
Date fromMs = new Date(1_700_000_000_000L);
long ms = now.getTime(); // milliseconds since epochnew Date() et Date.getTime() sont les deux méthodes qui ont bien vieilli. Tout le reste a soit été déprécié, soit comporte un piège.
Les accesseurs calendaires dépréciés
Ces méthodes ont été dépréciées dans Java 1.1 (1997) lorsque Calendar a été ajouté :
date.getYear(); // year - 1900 (deprecated)
date.getMonth(); // 0-11 (deprecated)
date.getDate(); // 1-31, day of month (deprecated)
date.getDay(); // 0-6, day of week (deprecated)
date.getHours(); date.getMinutes(); date.getSeconds(); // local-zone reads (deprecated)Ces dépréciations existent depuis 28 ans. Elles fonctionnent encore. Les pièges :
getYear()renvoieyear - 1900. Pour 2025, il renvoie125. C'est sans conteste un candidat à la décision d'API la plus étrange du JDK.getMonth()renvoie de 0 à 11. Janvier vaut 0, décembre vaut 11. Les bugs de décalage de un sont garantis si vous écrivezgetMonth() + 1et l'oubliez une fois.- Chaque accesseur lit dans le fuseau horaire par défaut de la JVM. Le même
Datesur deux machines dans des fuseaux différents donne des résultatsgetDate()différents.
N'appelez pas ces méthodes. Dès que vous vous surprenez à vouloir utiliser date.getYear(), convertissez en Instant/ZonedDateTime et utilisez les accesseurs modernes.
Le pont Date ↔ Instant
Date legacy = new Date();
Instant inst = legacy.toInstant(); // since Java 8
Instant other = Instant.parse("2025-11-04T19:30:00Z");
Date back = Date.from(other); // since Java 8toInstant() et Date.from(...) sont les méthodes de conversion modernes, ajoutées avec java.time. Ce sont les deux seuls appels à java.util.Date que vous devriez écrire dans du nouveau code.
La conversion est avec perte dans un sens : Date est en précision milliseconde, Instant en précision nanoseconde. Un aller-retour Instant → Date → Instant tronque les nanosecondes sous la milliseconde :
Instant high = Instant.parse("2025-11-04T19:30:00.123456789Z");
Instant low = Date.from(high).toInstant();
// low = 2025-11-04T19:30:00.123Z — the 456789 nanos are gonePour les horodatages serveur, c'est acceptable ; pour la capture d'événements à haute résolution, restez dans Instant de bout en bout.
java.sql.Date et java.sql.Timestamp
Les types JDBC sont des sous-classes de java.util.Date :
java.sql.Date— une date sans heure (le composant heure est forcé à 00:00:00 dans un certain fuseau). Nom trompeur ; c'est toujours un wrapper en millisecondes depuis l'époque en dessous.java.sql.Time— une heure sans date.java.sql.Timestamp— commeDatemais avec une précision nanoseconde.
Ils ont tous des méthodes de conversion JDK 8 :
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.of(2025, 11, 4));
LocalDate localDate = sqlDate.toLocalDate();
java.sql.Timestamp ts = java.sql.Timestamp.from(Instant.now());
Instant inst = ts.toInstant();Les pilotes JDBC modernes acceptent également les types java.time directement via setObject/getObject — pour le nouveau code, ignorez les types java.sql.* et utilisez LocalDate/Instant. Les conversions sont là pour le code qui doit interopérer avec un pilote ou un framework qui n'a pas encore migré.
Comparaison et tri
Date implémente Comparable<Date>. L'ordre est par millisecondes depuis l'époque — identique à Instant. Ainsi, trier une List<Date> fonctionne de la même manière que trier une List<Instant>.
equals compare le long sous-jacent. Deux valeurs Date sont égales si et seulement si elles ont la même valeur en millisecondes. Le hachage fonctionne (c'est (int)(time ^ (time >>> 32))), donc Date convient comme clé de HashMap — bien qu'encore une fois, Instant soit le choix moderne.
Mutabilité
Le piège caché le plus important : Date est mutable.
Date d = new Date();
d.setTime(0); // mutates d in placeCela signifie que toute méthode qui accepte ou retourne un Date est dangereuse — l'appelant peut modifier la valeur après l'avoir passée ; l'appelé peut modifier la valeur que l'appelant détient. Le code de bibliothèque copie défensivement (new Date(d.getTime())) chaque Date qu'il reçoit. C'est le genre de bookkeeping que java.time a totalement éliminé en rendant chaque type immuable.
Dans le code hérité, traitez tout champ Date comme s'il pouvait changer sous vos pieds. Convertissez en Instant à la frontière de l'API si vous avez besoin d'un instantané stable.
SimpleDateFormat : l'autre chose dépréciée
Associé à Date dans l'ancien code, on trouve java.text.SimpleDateFormat :
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date d = sdf.parse("2025-11-04");SimpleDateFormat n'est pas thread-safe. Le partager entre threads produira des sorties incorrectes, des exceptions, ou les deux, de manière intermittente. La solution de contournement standard dans le code hérité est ThreadLocal<SimpleDateFormat> ; le remplacement moderne est DateTimeFormatter, qui est thread-safe et peut être mis en cache.
Si vous maintenez du code avec un static SimpleDateFormat, traitez-le comme un bug connu, que quelqu'un ait signalé une défaillance ou non.
Exemple pratique : interopérabilité avec le code hérité
Le programme ci-dessous utilise Date comme vous le trouveriez dans une API héritée et montre le chemin de conversion vers java.time pour chaque opération. Lisez-le comme « voici la recette de migration » : chaque appel hérité a un équivalent en une ligne avec Instant ou ZonedDateTime.
Ce qu'il faut retenir de l'exécution :
Date.toString()s'affiche dans le fuseau horaire par défaut de la JVM. La même valeurDates'affiche différemment sur un serveur UTC et sur un laptop America/New_York. C'est le défaut de conception central qui a motivé la refonte avecjava.time— la valeur est en UTC, l'affichage est local, et l'API ne vous donne aucun moyen simple de savoir quelle vue vous regardez. Si vous tenez à préciser le fuseau, convertissez enZonedDateTimeet fournissez le fuseau explicitement.now.getYear()a renvoyé125pour 2025. La conventionyear - 1900était une erreur de Java 1.0 jamais corrigée ; la méthode a été dépréciée en 1.1 et est toujours là. Tout appel àgetYear()sur unDaterenvoyant « 125 » ou « 85 » est un bug qui attend de s'afficher à un utilisateur.Instant.parse("...nanosecondes")a affiché neuf chiffres de précision ; la même valeur aller-retour viaDate.fromet retour a perdu les six derniers. Pour les logs serveur (la précision milliseconde suffit), la troncature n'a pas d'importance. Pour « j'ai capturé cet événement avec un timing haute précision », ne faites pas d'aller-retour viaDate.- Le +1 jour en héritage était
now.getTime() + 24L * 60 * 60 * 1000— arithmétique manuelle, facile à mal taper (oublier leLet causer un dépassement). Le moderneinst.plus(Duration.ofDays(1))est sûr du point de vue des types et se lit à voix haute. Lors d'une migration, remplacer chaque calcultime + N * msparDurationest le fruit le plus facile à cueillir. - La démonstration de mutabilité à la fin a montré que
shared.setTime(0)changeait la valeur vue à travers les deux références. Dans une base de code multi-thread, c'est une condition de course ; en mono-thread, c'est quand même un rituel de copie défensive que le JDK imposait à chaque bibliothèque. L'API moderne ne demande jamais ce rituel.
Prochaine étape
java.util.Date est la moitié de l'API héritée. L'autre moitié est java.util.Calendar — la classe ajoutée dans Java 1.1 pour donner à Date les accesseurs calendaires que Date lui-même n'aurait pas dû avoir. Le prochain chapitre, Java Calendar Class, est le dernier de cette partie et le couvre avec la même forme de recette de migration : chaque appel hérité a un équivalent dans java.time.