W3docs

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) : atZone renvoie l'heure après la transition. LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York")) devient 03:30-04:00 — le JDK a avancé d'une heure pour atterrir sur une heure d'horloge valide.
  • Heure répétée (chevauchement) : atZone renvoie le premier des deux moments valides (celui avant le changement de décalage). Utilisez withEarlierOffsetAtOverlap() ou withLaterOffsetAtOverlap() 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 hours

Lors 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.

ObjectifMé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 + Offset

La distinction est nette :

  • isBefore/isAfter/isEqual comparent les moments sous-jacents (Instants).
  • equals compare la structure complète — deux ZonedDateTimes qui représentent le même moment mais ont des fuseaux différents ne sont pas equal.

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.

java— editable, runs on the server

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 jumelle withZoneSameLocal(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) valait false même si les deux représentaient le même moment. equals compare la structure complète (date-heure locale + fuseau). Pour « même moment quelle que soit l'étiquette », utilisez isEqual ou comparez des Instants. C'est exactement la même distinction que LocalDate.equals versus isEqualequals pour « même objet-valeur », isEqual pour « 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, utilisez ZoneRules.getTransition(localDateTime) et vérifiez si l'objet retourné est un écart.
  • Le chevauchement DST (2025-11-02 01:30 à NY) vous a donné deux ZonedDateTimes distincts avec les mêmes champs locaux et des décalages différents — EDT contre EST, à une heure d'intervalle. withLaterOffsetAtOverlap() et withEarlierOffsetAtOverlap() 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) et plus(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. Utilisez plusDays/plusWeeks pour la planification en forme de calendrier (« même heure demain ») et plus(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.

Exercices

Pratique
Vous planifiez une réunion récurrente à 09:00 America/New_York et stockez la prochaine occurrence avec `nextMeeting.plusDays(7)`. La semaine qui traverse la transition DST de passage à l'heure d'été, qu'est-ce qui est vrai du résultat ?
Vous planifiez une réunion récurrente à 09:00 America/New_York et stockez la prochaine occurrence avec `nextMeeting.plusDays(7)`. La semaine qui traverse la transition DST de passage à l'heure d'été, qu'est-ce qui est vrai du résultat ?
Was this page helpful?