Java ZonedDateTime
Représentez des dates-heures avec fuseau en Java grâce à ZonedDateTime et la classe ZoneId.
ZonedDateTime est un LocalDateTime auquel est associé un ZoneId. Il exprime : « cette date calendaire et cette heure, dans cet endroit. » La combinaison identifie un moment unique sur la ligne de temps globale — 2025-11-04T14:00 [America/New_York] correspond exactement à un Instant, distinct de 2025-11-04T14:00 [Europe/Berlin].
C'est la classe à utiliser chaque fois que l'heure locale d'un événement dans un lieu précis est importante. Les calendriers de réunion. Les tâches planifiées qui doivent se déclencher à « 9h dans le fuseau de l'utilisateur ». Tout ce qui doit survivre à une transition heure d'été (DST). LocalDateTime ne dispose pas de suffisamment d'informations ; Instant est en UTC et ne transporte pas l'étiquette de fuseau significative pour un humain. ZonedDateTime combine les deux.
ZoneId : le catalogue des fuseaux
Avant ZonedDateTime, découvrons le ZoneId lui-même — un fuseau est identifié par un ZoneId, que l'on obtient avec ZoneId.of(...) :
ZoneId ny = ZoneId.of("America/New_York");
ZoneId de = ZoneId.of("Europe/Berlin");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZoneId utc = ZoneId.of("UTC");
ZoneId sys = ZoneId.systemDefault();Les chaînes sont des identifiants de la base de données des fuseaux horaires IANA (Region/City). La liste complète est accessible via ZoneId.getAvailableZoneIds() — environ 600 entrées, mises à jour périodiquement lorsque des pays modifient leurs fuseaux ou leurs règles DST. ZoneId contient l'historique IANA, de sorte que les dates de 1985 utilisent les règles en vigueur en 1985.
Évitez ZoneOffset (un décalage fixe ±HH:MM) lorsque vous voulez désigner un vrai fuseau. ZoneOffset.of("-05:00") est correct pour New York en novembre et incorrect en juin ; ZoneId.of("America/New_York") est correct toute l'année.
Les noms de fuseaux à trois lettres comme "EST" et "PST" sont désormais surtout des alias, ambigus (était-ce l'Eastern Standard ou l'Eastern Australia ?), et silencieusement dépréciés. Utilisez Region/City. "UTC" et "GMT" sont des cas particuliers et sont acceptables.
Création
ZonedDateTime now = ZonedDateTime.now(); // system zone
ZonedDateTime nowNY = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime made = ZonedDateTime.of(2025, 11, 4, 14, 0, 0, 0, ZoneId.of("America/New_York"));
ZonedDateTime parsed = ZonedDateTime.parse("2025-11-04T14:00:00-05:00[America/New_York]");Le chemin de construction le plus courant est « j'ai un LocalDateTime, j'ai un ZoneId, je les associe » :
LocalDateTime ldt = LocalDateTime.of(2025, 11, 4, 14, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));atZone(zone) est le pont en un seul appel entre une lecture d'horloge locale et un moment avec fuseau. Il gère également les deux cas particuliers introduits par le DST.
DST : quand l'horloge saute ou se répète
Deux fois par an, l'horloge murale de tout fuseau appliquant le DST saute ou se répète. Lorsqu'elle avance — aux États-Unis, 02:00 passe à 03:00 un dimanche de mars — les horaires entre 02:00 et 03:00 n'existent pas ce jour-là. Lorsqu'elle recule, les horaires entre 01:00 et 02:00 se produisent deux fois. ZonedDateTime doit gérer les deux cas, et son comportement est documenté :
- Heure ignorée (écart) :
atZonerenvoie l'heure après la transition.LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York"))devient03:30-04:00— le JDK a avancé d'une heure pour atterrir sur une heure d'horloge valide. - Heure répétée (chevauchement) :
atZonerenvoie le premier des deux moments valides (celui avant le changement de décalage). UtilisezwithEarlierOffsetAtOverlap()ouwithLaterOffsetAtOverlap()pour choisir explicitement.
ZonedDateTime ambiguous = LocalDateTime.of(2025, 11, 2, 1, 30)
.atZone(ZoneId.of("America/New_York")); // 01:30 EDT (earlier)
ZonedDateTime explicit = ambiguous.withLaterOffsetAtOverlap(); // 01:30 EST (later)Les deux ZonedDateTimes ont le même LocalDateTime mais des décalages différents et des Instants différents. C'est le seul endroit dans java.time où la même lecture d'horloge locale correspond légitimement à deux moments — et c'est la source des bogues liés au DST que vous avez peut-être rencontrés. Soyez délibéré lorsque le chevauchement est important.
Décomposition
ZoneId zone = zdt.getZone();
ZoneOffset offset = zdt.getOffset();
LocalDateTime ldt = zdt.toLocalDateTime();
LocalDate date = zdt.toLocalDate();
LocalTime time = zdt.toLocalTime();
Instant inst = zdt.toInstant();
OffsetDateTime odt = zdt.toOffsetDateTime();Les accesseurs se répartissent en trois groupes : la partie fuseau (getZone, getOffset), la partie horloge locale (toLocalDateTime, toLocalDate, toLocalTime), et la partie moment global (toInstant). Les trois sont simultanément vraies du même ZonedDateTime ; vous choisissez la projection dont vous avez besoin.
OffsetDateTime est un type apparenté — LocalDateTime plus un ZoneOffset (pas de fuseau, pas de DST). Il est utile pour sérialiser « 2025-11-04T14:00-05:00 » sans s'engager sur un fuseau nommé (souvent ce que veulent les timestamps JSON) ; pour tout code nécessitant une arithmétique consciente du DST, conservez le ZonedDateTime.
Deux sens de « jour suivant »
ZonedDateTime dispose de deux méthodes qui semblent similaires mais ne le sont pas :
zdt.plusDays(1); // add 1 day to the local clock reading
zdt.plus(Duration.ofHours(24)); // add exactly 24 hoursLors d'un jour de transition DST, les deux divergent. Le jour où les horloges avancent, plusDays(1) atterrit à la même heure locale le lendemain (soit seulement 23 heures de temps réel). plus(Duration.ofHours(24)) atterrit à une heure d'horloge murale une heure plus tard que la veille.
| Objectif | Méthode |
|---|---|
| « Même heure demain » (calendrier) | plusDays(1) |
| « Exactement 24 heures à partir de maintenant » (durée) | plus(Duration.ofHours(24)) |
Les deux sont corrects ; ils répondent à des questions différentes. Choisissez délibérément.
Comparaisons et égalité
zdt1.isBefore(zdt2); // compares Instants
zdt1.isAfter(zdt2);
zdt1.isEqual(zdt2); // compares Instants
zdt1.equals(zdt2); // compares LocalDateTime + Zone + OffsetLa distinction est nette :
isBefore/isAfter/isEqualcomparent les moments sous-jacents (Instants).equalscompare la structure complète — deuxZonedDateTimes qui représentent le même moment mais ont des fuseaux différents ne sont pasequal.
Pour « ces deux valeurs représentent-elles le même moment indépendamment du fuseau », utilisez isEqual ou convertissez les deux en Instant et comparez.
Exemple concret : une réunion entre trois bureaux
Le programme ci-dessous planifie une réunion à 14:00 heure de Berlin et calcule l'heure correspondante dans les bureaux de New York et de Tokyo. Il planifie ensuite une réunion hebdomadaire récurrente qui survit à une transition DST, illustrant la différence entre plusDays(7) et plus(Duration.ofDays(7)) sur une semaine de transition.
Ce qu'il faut retenir de l'exécution :
withZoneSameInstant(otherZone)est l'opération pour « quelle heure est-il dans leur bureau ? » — elle fixe le moment et réaffiche l'horloge murale dans le nouveau fuseau. Sa jumellewithZoneSameLocal(otherZone)fixe l'horloge murale et change le moment (la réunion se déplace). Les noms sont facilement confondus ; la différence tient à ce qui reste fixe. Lisez-les attentivement lorsque vous les écrivez.berlin.equals(ny)valaitfalsemême si les deux représentaient le même moment.equalscompare la structure complète (date-heure locale + fuseau). Pour « même moment quelle que soit l'étiquette », utilisezisEqualou comparez desInstants. C'est exactement la même distinction queLocalDate.equalsversusisEqual—equalspour « même objet-valeur »,isEqualpour « même point dans le temps ».- L'écart DST (
2025-03-09 02:30à NY) a été résolu en avançant à03:30-04:00. Le JDK n'a pas lancé d'exception ; il a choisi le moment post-transition. Si vous devez absolument détecter que vous avez fourni une heure impossible, utilisezZoneRules.getTransition(localDateTime)et vérifiez si l'objet retourné est un écart. - Le chevauchement DST (
2025-11-02 01:30à NY) vous a donné deuxZonedDateTimes distincts avec les mêmes champs locaux et des décalages différents —EDTcontreEST, à une heure d'intervalle.withLaterOffsetAtOverlap()etwithEarlierOffsetAtOverlap()permettent de choisir. Si vous stockez des événements planifiés, décidez à l'avance lequel des deux l'utilisateur veut et appliquez le bon appel au moment de l'analyse. plusDays(1)etplus(Duration.ofHours(24))ont produit des résultats différents le jour du passage à l'heure d'été — 23 heures de temps réel contre 24, atterrissant à des heures d'horloge murale différentes. UtilisezplusDays/plusWeekspour la planification en forme de calendrier (« même heure demain ») etplus(Duration)pour l'arithmétique en temps écoulé (« alarme dans 24 heures »). Le choix correspond presque toujours à l'intention côté utilisateur.
La suite
ZonedDateTime est le côté humain de « un moment avec une étiquette ». Le chapitre suivant, Java Instant, est le côté machine — un moment exprimé en nanosecondes depuis l'époque, sans fuseau, sans calendrier, le type utilisé sur le réseau par tous les systèmes distribués.