Java Instant
Représentez un moment sur la timeline en UTC avec Instant — idéal pour les horodatages et l'heure machine en Java.
Instant est un moment unique sur la timeline globale, stocké sous forme de nanosecondes depuis l'époque Unix (1970-01-01T00:00:00Z). Il n'a pas de fuseau horaire, pas de calendrier, pas de notion de « quel jour sommes-nous » — juste un compteur. Ce compteur est en UTC par construction, donc deux Instants provenant de machines différentes dans des fuseaux différents se comparent directement : celui avec la valeur la plus faible est antérieur.
C'est le type pour les horodatages machine. Les lignes de journaux. Les horodatages de messages. « Quand le serveur a-t-il reçu la requête. » Les pistes d'audit. Tout ce qui doit être triable globalement et qui ne doit jamais être ambigu quant au jour qu'il représente — car il n'y a pas de « jour » du tout, seulement des secondes.
Création
Instant now = Instant.now(); // current moment, from System clock
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
Instant max = Instant.MAX; // year +1000000000
Instant min = Instant.MIN; // year -1000000000
Instant fromS = Instant.ofEpochSecond(1_700_000_000L);
Instant fromMs = Instant.ofEpochMilli(1_700_000_000_000L);
Instant fromIso = Instant.parse("2025-11-04T19:30:00Z"); // ISO-8601, trailing Z is mandatoryLa forme textuelle se termine par un Z littéral (pour « Zulu time », terme militaire pour UTC). Instant.parse("2025-11-04T19:30:00") (sans Z) est une erreur de parsing — le type refuse de deviner quel fuseau horaire vous vouliez.
Deux méthodes factory que vous utiliserez souvent :
Instant.ofEpochSecond(epochSec); // long seconds, no nanos
Instant.ofEpochSecond(epochSec, nanos); // with sub-second resolutionLa plupart des formats d'horodatage externes (Unix time(2), syslog, entiers JSON created_at) sont en secondes ou millisecondes depuis l'époque. Les factory ofEpochSecond / ofEpochMilli sont le pont standard.
Résolution
Instant est précis à la nanoseconde (1 seconde = 1 000 000 000 ns). Sur la plupart des systèmes, l'horloge sous-jacente a une résolution inférieure — la milliseconde est typique, la microseconde sur Linux moderne. Instant.now() retourne une valeur à la résolution disponible ; les nanosecondes inutilisées sont à zéro.
Accesseurs :
long seconds = inst.getEpochSecond(); // long; can go past 2038
int nanos = inst.getNano(); // 0-999_999_999
long milli = inst.toEpochMilli(); // throws if out of long rangetoEpochMilli est la conversion avec perte : les nanosecondes sont tronquées en millisecondes. Pour les lignes de journaux et les horodatages JSON, c'est généralement acceptable ; pour les enregistrements d'événements haute fréquence, utilisez getEpochSecond + getNano séparément.
Pas de calendrier
Instant.getDayOfMonth() n'existe pas. Ni getYear, getHour, ni aucun des accesseurs calendaires. Le type ne sait genuinement pas — les informations calendaires nécessitent un fuseau horaire, et Instant n'en a pas. Si vous voulez savoir « à quelle heure était-il à New York quand cela s'est produit », vous devez d'abord attacher un fuseau horaire :
ZonedDateTime zdt = inst.atZone(ZoneId.of("America/New_York"));
int hour = zdt.getHour();
LocalDate date = zdt.toLocalDate();atZone(zone) est le pont dans l'autre direction par rapport à ZonedDateTime.toInstant(). Les deux ensemble vous donnent le parcours complet : moment ↔ label zoné. Consultez Java ZonedDateTime pour le côté calendrier de cette association.
Arithmétique
Même forme fluide :
inst.plusSeconds(60);
inst.plusMillis(500);
inst.plusNanos(1_000_000);
inst.plus(Duration.ofMinutes(15)); // any Duration
inst.minus(Duration.ofDays(1)); // exactly 24h * 3600sPas de plusDays sur Instant (au sens calendaire). Il dispose de plus(amount, ChronoUnit), et ChronoUnit.DAYS fonctionne car le JDK définit un Day comme exactement 24 heures de secondes pour Instant. Ce n'est pas ce qu'est un jour calendaire quand le changement d'heure est en jeu, ce qui est la raison pour laquelle Instant ne prétend pas en être un.
inst.plus(1, ChronoUnit.DAYS); // exactly 86_400 seconds
inst.plus(7, ChronoUnit.DAYS); // exactly 604_800 secondsPour les opérations de forme calendaire (« un mois plus tard dans le fuseau de l'utilisateur »), passez par ZonedDateTime :
Instant later = inst.atZone(zone).plusMonths(1).toInstant();Comparaison
inst1.isBefore(inst2);
inst1.isAfter(inst2);
inst1.equals(inst2);
inst1.compareTo(inst2);Instant implémente Comparable<Instant> avec un ordre naturel par seconde d'époque puis par nanosecondes. equals est simple : même seconde et mêmes nanosecondes.
Distance
Duration d = Duration.between(start, end); // a Duration
long millis = ChronoUnit.MILLIS.between(start, end);
long days = ChronoUnit.DAYS.between(start, end); // 24h-equivalent daysPour les horodatages machine, tout cela est exact — il n'y a pas d'ambiguïté calendaire. ChronoUnit.MONTHS.between(start, end) sur des Instants lève une exception car les mois n'ont pas une durée constante en secondes : il n'y a pas de fuseau horaire, donc le calculateur n'a aucun moyen de savoir quels mois contiennent ces secondes. C'est le bon mode d'échec.
Pont java.util.Date
L'ancien code utilise java.util.Date. Les conversions sont directes :
Date legacy = Date.from(inst); // Instant -> Date
Instant back = legacy.toInstant(); // Date -> InstantDate est en interne un wrapper autour d'un long de millisecondes d'époque, donc l'aller-retour est sans perte modulo les nanosecondes (Date a une précision milliseconde, Instant a une précision nanoseconde). Le chapitre Legacy Date couvre la migration en détail.
Pourquoi tout ce qui est interne devrait être Instant
La recommandation qui a émergé de l'utilisation de java.time en production depuis dix ans :
- En interne, utilisez
Instant. Stockage, comparaison, journalisation, horodatages de messages, partout où la valeur circule de machine en machine. - À la frontière — lors de l'affichage à un utilisateur, lors de la réception d'une saisie utilisateur — convertissez en
ZonedDateTimeouLocalDateTimeen utilisant le bon fuseau horaire pour le contexte.
Cela sépare « ce qui s'est vraiment passé » de « comment c'est libellé ». Un bug à la frontière (mauvais fuseau horaire) laisse vos valeurs internes correctes ; un bug dans le système de types qui permet à LocalDateTime de circuler en interne tend à vous laisser avec des horodatages qui sont silencieusement dans des fuseaux différents.
Un exemple concret : un petit journal d'événements
Le programme ci-dessous enregistre une séquence d'événements en tant qu'Instants, calcule les durées inter-événements, démontre la frontière calendaire en attachant un fuseau horaire pour l'affichage, montre le pont Date hérité, et illustre finalement la règle « pas de calendrier » en montrant que ChronoUnit.MONTHS.between sur deux Instants lève une exception.
Ce qu'il faut retenir de l'exécution :
Instant.parse("2025-11-04T19:30:00Z")n'a été parsé que grâce auZfinal. Supprimez leZet le parsing échoue — le type insiste pour savoir que la chaîne est en UTC. Les autres fuseaux doivent passer parZonedDateTime.parse(ou être fournis viaatZone).- La séquence d'événements a utilisé
Duration.between(...). Chaque résultat était un nombre entier propre de millisecondes — pas de confusion de fuseau horaire, pas de changement d'heure, pas d'arithmétique calendaire. C'est pourquoi le chronométrage côté serveur appartient àInstant: l'arithmétique est juste une soustraction delongs sous le capot. - Le bloc « même instant, deux labels de fuseau » a affiché des
ZonedDateTimes qui semblaient différents mais étaient le mêmeInstant.atZone(...)est purement une opération d'affichage surInstant. Si vous voulez faire des calculs calendaires (mois suivant, fin de semaine), faites-les sur leZonedDateTime, puis appelez.toInstant()pour revenir. Date.from(inst)etlegacy.toInstant()étaient sans perte modulo les nanosecondes.Datene porte que des millisecondes, donc un aller-retour viaDatetronque la précision sous la milliseconde. Pour la plupart des journaux, c'est acceptable ; pour la capture d'événements haute précision, restez dansInstantde bout en bout et ne passez jamais parDate.ChronoUnit.MONTHS.between(a, b)a levéUnsupportedTemporalTypeException. C'est le bon mode d'échec : les mois ne sont pas des secondes de longueur constante, et le JDK refuse d'inventer une réponse. Passer parZonedDateTimea fourni le fuseau horaire manquant, et le même appel a fonctionné. Le schéma est général : les opérations de forme calendaire nécessitent un fuseau horaire, et le système de types vous oblige à en fournir un explicitement.
Et ensuite
Instant est le moment. Les deux prochains chapitres couvrent les durées entre les moments : Java Duration pour les mesures « X secondes, Y nanosecondes », et Java Period pour les durées calendaires « X années, Y mois, Z jours ». Les deux ensemble vous permettent d'exprimer « une heure plus tard » vs « un mois plus tard » sans perte.