1. Notion de fuseau horaire
Il existe de nombreux fuseaux horaires dans le monde : lorsqu’il est midi à Minsk, il n’est que le matin à New York, et déjà le soir à Tokyo. Si vous stockez la date et l’heure sans prise en compte du fuseau horaire, la confusion est facile : par exemple, si votre serveur est en Allemagne et l’utilisateur à Vladivostok, l’affichage "2025-06-01 12:00" signifiera des choses totalement différentes pour chacun.
Un fuseau horaire (timezone) — c’est la règle qui détermine combien de temps ajouter ou soustraire à l’heure de Greenwich (UTC) pour obtenir l’heure « locale » d’une région donnée.
En Java, la classe ZoneId est utilisée pour gérer les fuseaux horaires. Voici quelques exemples d’identifiants de zones :
- "Europe/Minsk"
- "UTC"
- "America/New_York"
- "Asia/Tokyo"
Pourquoi est-ce important ?
- Affichage correct de l’heure pour des utilisateurs de différents pays.
- Enregistrement correct de l’heure des événements (par exemple, journalisation, réservation de billets, échéances).
- Prise en compte du passage à l’heure d’été/d’hiver (merci, l’Europe !).
2. ZonedDateTime — date et heure tenant compte du fuseau horaire
ZonedDateTime est une classe qui stocke la date, l’heure et les informations de fuseau horaire. C’est comme LocalDateTime, mais il « sait » aussi dans quelle région il se trouve.
Création de ZonedDateTime
Date et heure actuelles dans le fuseau horaire du système
import java.time.ZonedDateTime;
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // Par exemple: 2025-06-01T15:30:00+03:00[Europe/Minsk]
Heure dans un fuseau horaire spécifique
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("Minsk : " + MinskTime);
System.out.println("New York : " + newYorkTime);
Création à partir de LocalDateTime
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]
Récupération et définition du fuseau horaire
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("Tokyo : " + tokyoTime);
Conversion entre zones : withZoneSameInstant()
Il arrive que l’on doive savoir à quoi ressemble le même événement dans une autre zone. Pour cela, on utilise withZoneSameInstant() :
ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("Heure de la réunion à Minsk : " + MinskMeeting);
System.out.println("Le même événement à New York : " + newYorkMeeting);
Attention : withZoneSameInstant() convertit l’heure de façon à ce qu’elle corresponde au même moment dans l’autre zone. Si vous utilisez withZoneSameLocal(), la date et l’heure locales restent les mêmes mais la zone change — c’est presque toujours une erreur !
3. Instant — un point absolu dans le temps
Instant est une classe qui représente un moment absolu, indépendamment du fuseau horaire. Techniquement, c’est le nombre de secondes et de nanosecondes écoulées depuis le 1er janvier 1970 à Greenwich (UTC). Si le temps avait un passeport — Instant en serait le numéro.
Création d’un Instant
import java.time.Instant;
Instant now = Instant.now();
System.out.println(now); // Par exemple: 2025-06-01T12:30:00.123Z
Remarquez la lettre Z — cela signifie « Zulu time », c’est-à-dire UTC.
Création à partir des secondes depuis l’époque Unix
Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z
Conversion Instant ↔ ZonedDateTime/LocalDateTime
De ZonedDateTime vers Instant
ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);
De Instant vers ZonedDateTime
ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);
De Instant vers LocalDateTime
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);
4. Pratique : heure actuelle dans différents fuseaux, conversion entre zones
Obtenir l’heure actuelle dans différents fuseaux
Faisons une mini-application qui affiche l’heure actuelle à Minsk, New York et Tokyo :
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class TimeZonesDemo {
public static void main(String[] args) {
ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("Minsk : " + Minsk);
System.out.println("New York : " + newYork);
System.out.println("Tokyo : " + tokyo);
}
}
Conversion de l’heure entre les zones
Supposons que vous ayez un événement prévu à 18:00 à Minsk. Comment savoir à quelle heure ce sera à New York et à Tokyo ?
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MeetingTime {
public static void main(String[] args) {
LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Réunion à Minsk : " + minskEvent);
System.out.println("À New York : " + newYorkEvent);
System.out.println("À Tokyo : " + tokyoEvent);
}
}
Conversion de LocalDateTime en ZonedDateTime et inversement
Local → Zoned :
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(zonedTime);
Zoned → Local :
LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);
5. Points importants et subtilités
Pourquoi il ne faut pas stocker seulement LocalDateTime
LocalDateTime, c’est simplement une date et une heure sans fuseau horaire. Pour la plupart des logiques métier, c’est insuffisant ! Par exemple, si vous stockez "2025-06-01 12:00" comme LocalDateTime, alors pour un utilisateur de Minsk et un utilisateur de New York ce seront des moments totalement différents dans le temps réel.
Stockez toujours le temps absolu (par exemple, un Instant), ou l’heure avec fuseau (ZonedDateTime) si l’événement est réellement lié à une zone précise. LocalDateTime n’est bon que si vous travaillez avec des dates « flottantes » (par exemple, un anniversaire sans prise en compte de l’heure du jour ni du fuseau).
Problèmes liés au passage à l’heure d’été/d’hiver
Les fuseaux horaires — ce n’est pas seulement un décalage par rapport à UTC, mais aussi des règles de passage à l’heure d’été/d’hiver. Par exemple, dans certains pays, à une date donnée on avance ou recule les horloges d’une heure — et si vous stockez seulement un LocalDateTime, vous ne saurez pas si cette heure a même existé.
Exemple de « trou temporel » :
- Aux États-Unis, en mars, à 2:00 du matin, on passe à 3:00.
- L’heure "2025-03-10 02:30" à New York n’a pas existé !
En travaillant avec ZonedDateTime, vous êtes protégé contre ce genre de surprises : la bibliothèque vérifiera elle-même la validité de l’heure.
Schéma : relations entre LocalDateTime, ZonedDateTime, Instant
graph TD
A["LocalDateTime
(date + heure, sans zone)"] -->|+ ZoneId| B["ZonedDateTime
(date + heure + zone)"]
B -->|"toInstant()"| C["Instant
(temps absolu, UTC)"]
C -->|"atZone(ZoneId)"| B
B -->|"toLocalDateTime()"| A
6. Erreurs courantes avec ZonedDateTime et Instant
Erreur n° 1 : utiliser LocalDateTime pour des événements globaux.
Si vous stockez la date et l’heure d’une réunion entre utilisateurs de pays différents comme un LocalDateTime, chacun verra son « 12:00 », alors qu’il s’agit de moments différents. Pour des événements globaux, utilisez ZonedDateTime ou Instant.
Erreur n° 2 : ignorer le fuseau lors de l’analyse d’une chaîne.
Si vous analysez la chaîne "2025-06-01T12:00:00" sans indiquer de zone, vous obtiendrez un LocalDateTime, et non un ZonedDateTime. Pour obtenir un ZonedDateTime, utilisez des chaînes contenant la zone ou ajoutez-la explicitement.
Erreur n° 3 : mauvaise conversion entre les zones.
Utiliser withZoneSameLocal() au lieu de withZoneSameInstant() peut conduire à une heure incorrecte. Utilisez toujours withZoneSameInstant() si vous souhaitez obtenir le même moment dans une autre zone.
Erreur n° 4 : ne pas tenir compte du passage à l’heure d’été/d’hiver.
Si vous planifiez des événements à la frontière d’un changement d’heure, utilisez impérativement ZonedDateTime et faites confiance à la bibliothèque — elle connaît tous les changements et « trous » temporels.
Erreur n° 5 : comparer des ZonedDateTime sans tenir compte de la zone.
Deux ZonedDateTime avec des zones différentes mais la même heure locale peuvent représenter des moments différents. Pour comparer, utilisez toInstant().
GO TO FULL VERSION