ZonedDateTime — qu’est-ce que c’est, travailler avec les fuseaux horaires et le temps

Auteur : IT Sectr Publié le : 2026-07-13 Temps de lecture : 10 min

ZonedDateTime est une classe immuable du package java.time qui stocke la date et l’heure avec les informations de fuseau horaire (ZoneId). Contrairement à LocalDateTime, ZonedDateTime identifie de manière univoque un instant sur la ligne du temps. Selon les spécifications d’Oracle Java 17 (2024), la classe gère correctement les transitions d’heure d’été (DST) via les règles de fuseau de la base de données IANA Time Zone Database.

Points Clés

  • ZonedDateTime est une classe immuable qui combine date, heure et fuseau horaire (ZoneId) en un seul objet.
  • Contrairement à LocalDateTime, ZonedDateTime définit de manière univoque un instant sur la ligne du temps et convient aux systèmes globaux.
  • La classe gère automatiquement les transitions d’heure d’été (DST) selon les règles de la base de données IANA Time Zone Database.
  • Pour convertir entre fuseaux horaires, utilisez la méthode withZoneSameInstant(ZoneId).
  • Il est recommandé de stocker ZonedDateTime dans les bases de données via OffsetDateTime ou TIMESTAMP WITH TIME ZONE.

Qu’est-ce que ZonedDateTime ?

ZonedDateTime est l’une des classes clés du package java.time, représentant la date et l’heure avec des informations complètes de fuseau horaire. Elle combine trois composants : LocalDateTime (date et heure), ZoneId (identifiant de fuseau) et ZoneOffset (décalage par rapport à UTC).

Contrairement à LocalDateTime, qui stocke seulement l’heure murale (wall-clock time) sans liaison de fuseau, ZonedDateTime identifie de manière univoque un instant. Deux instances identiques de LocalDateTime dans des fuseaux horaires différents représentent des instants différents. Deux instances identiques de ZonedDateTime représentent le même instant.

La classe est complètement immuable et thread-safe. Toutes les opérations arithmétiques retournent un nouvel objet. ZonedDateTime implémente l’interface ChronoZonedDateTime et peut être utilisée partout où un traitement horaire zoné est nécessaire en Java.

Selon les spécifications d’Oracle Java 17, ZonedDateTime prend en charge le travail avec n’importe quel fuseau de la base de données IANA Time Zone Database, qui comprend plus de 600 fuseaux horaires.

ZonedDateTime vs LocalDateTime : quelle est la différence ?

La différence principale — ZonedDateTime contient un fuseau horaire, tandis que LocalDateTime n’en contient pas. Cette différence fondamentale détermine le champ d’application de chaque classe.

LocalDateTime est utilisé pour les événements locaux : heure de concert, emploi du temps, date de naissance. Si un événement se produit à Moscou à 15h00, LocalDateTime enregistrera 15h00 sans aucune liaison. Si vous déplacez le serveur à New York, l’heure restera 15h00 — mais ce sera un instant physique différent.

ZonedDateTime est utilisé pour les données globales : journaux serveur, horodatages API, réunions internationales. Si une réunion est prévue à 15h00 MSK, ZonedDateTime préservera à la fois l’heure et le fuseau. À New York, elle s’affichera correctement comme 8h00 EST. Selon Baeldung (2024), le choix entre LocalDateTime et ZonedDateTime est la décision architecturale la plus courante lors du travail avec les dates.

Règle pratique : si les données sont stockées pour une seule région — utilisez LocalDateTime. Si les données traversent les frontières des fuseaux horaires — utilisez ZonedDateTime. Si vous devez représenter un instant absolu — utilisez Instant.

Comment fonctionne le fuseau horaire dans java.time ?

Le fuseau horaire dans java.time est représenté par la classe ZoneId. ZoneId est un identifiant de fuseau au format « continent/région », par exemple « Europe/Moscow », « America/New_York », « Asia/Tokyo ». ZoneId est obtenu via la méthode statique of(String zoneId) ou via le fuseau horaire par défaut du système.

ZoneId est divisé en deux types : fixed offset (décalage fixe, ex. « +03:00 ») et region-based (fuseaux régionaux, ex. « Europe/London »). Les fuseaux régionaux contiennent des règles de passage à l’heure d’été et des changements historiques. Fixed offset est simplement un décalage fixe.

Pour obtenir le décalage actuel d’un ZoneId à un instant donné, utilisez la méthode getRules(), qui retourne ZoneRules. ZoneRules contient toutes les transitions et décalages pour un fuseau donné. C’est le mécanisme clé pour la gestion correcte du DST.

Tous les fuseaux horaires sont fournis avec le JDK via les fichiers tzdata (IANA Time Zone Database) et sont mis à jour régulièrement. Sous Android, la version tzdata dépend des mises à jour système via Google Play Services.

Création de ZonedDateTime

ZonedDateTime peut être créé de plusieurs façons. La plus simple est now(), qui retourne l’heure actuelle dans le fuseau horaire du système. La variante now(ZoneId) permet d’obtenir l’heure actuelle dans un fuseau spécifié.

La méthode of(LocalDateTime, ZoneId) crée un ZonedDateTime à partir de l’heure locale et du fuseau. La variante of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) crée à partir des composants.

LocalDateTime peut être converti en ZonedDateTime via la méthode atZone(ZoneId). Instant — via Instant.atZone(ZoneId). Date — via Date.toInstant().atZone(ZoneId).

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

Conversion entre fuseaux horaires

La méthode de conversion principale est withZoneSameInstant(ZoneId). Elle convertit un ZonedDateTime dans un autre fuseau horaire tout en conservant le même instant. Par exemple, 15h00 MSK → 8h00 EST. La méthode withZoneSameLocal(ZoneId) change le fuseau tout en conservant l’heure locale — cela produit un instant différent.

Pour obtenir le décalage par rapport à UTC, utilisez la méthode getOffset(), qui retourne ZoneOffset. ZoneOffset est une sous-classe de ZoneId qui représente un décalage fixe au format « +HH:mm » ou « -HH:mm ».

La conversion en Instant s’effectue via la méthode toInstant(). Instant est un instant absolu dans le temps, indépendant du fuseau horaire. La conversion inverse est Instant.atZone(ZoneId).

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

Gestion de l’heure d’été (DST)

Les transitions d’heure d’été créent deux problèmes : les trous (gaps) et les chevauchements (overlaps). Un trou se produit au printemps lorsque les horloges sont avancées — une certaine heure n’existe pas. Un chevauchement se produit à l’automne lorsque les horloges sont reculées — la même heure se produit deux fois.

ZonedDateTime gère ces situations via une stratégie de résolution. Lors de la création d’un objet pendant un trou, java.time décale automatiquement l’heure du montant du décalage. Lors de la création pendant un chevauchement, la première option (avant la transition) est sélectionnée. Ce comportement peut être modifié via withZoneSameInstant.

Vous pouvez vérifier si une heure est en DST via zone.getRules().isDaylightSavings(instant). La méthode getOffset() montre le décalage réel pour un instant donné, et getRules().getDaylightSavings(instant) montre le montant d’ajustement DST en millisecondes.

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("Heure d’été", "Décalage heure d’été: $dstAmount")
    }
}

Formatage de ZonedDateTime

Pour formater ZonedDateTime, utilisez DateTimeFormatter. Le format ISO standard inclut la date, l’heure et le décalage : « 2026-07-21T15:30:00+03:00[Europe/Moscow] ». Formats prédéfinis : ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Pour une sortie localisée, utilisez DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle peut être SHORT, MEDIUM, LONG, FULL. LONG inclut le nom du fuseau (« MSK »), FULL inclut le nom complet (« Moscow Standard Time »).

Important : lors de l’analyse d’une chaîne avec ZonedDateTime, le format doit contenir les informations de fuseau ou de décalage. Si le fuseau n’est pas spécifié, utilisez LocalDateTime.parse() puis atZone().

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

ZonedDateTime dans Android : exemples pratiques

Le premier exemple — afficher l’heure d’une réunion pour l’utilisateur dans son fuseau horaire local. Le serveur envoie ZonedDateTime en UTC, le client convertit dans le fuseau horaire local de l’appareil.

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

Le deuxième exemple — calculer le temps jusqu’au prochain événement en tenant compte du fuseau horaire. Nous utilisons ZonedDateTime pour l’heure serveur et Duration.between() pour calculer la différence.

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

Le troisième exemple — travailler avec l’API Retrofit. Le serveur retourne une chaîne ISO-8601 avec fuseau. Nous utilisons un désérialiseur personnalisé pour convertir en ZonedDateTime.

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

Erreurs courantes avec les fuseaux horaires

La première erreur — utiliser ZoneId.systemDefault() dans le code serveur. Le fuseau horaire du serveur peut différer de celui du client, et l’utilisation du fuseau système sur le serveur conduit à des calculs incorrects. Spécifiez toujours le fuseau explicitement ou utilisez UTC comme référence.

La deuxième erreur — ignorer le DST lors du calcul de la durée. Duration.between() gère correctement les transitions, mais si vous soustrayez les horodatages manuellement, l’heure d’été peut causer une erreur d’une heure. Utilisez ChronoUnit.HOURS.between() au lieu de calculs manuels.

La troisième erreur — confondre withZoneSameInstant et withZoneSameLocal. Le premier change le fuseau en conservant l’instant — l’heure se décale. Le second change le fuseau en conservant l’heure locale — l’instant change. Choisir la mauvaise méthode est l’une des erreurs les plus courantes selon SonarSource (2024).

La quatrième erreur — supposer que le fuseau horaire de l’appareil est toujours le même que celui de l’utilisateur. L’utilisateur peut voyager et s’attendre à ce que l’application affiche l’heure dans son fuseau « d’origine » plutôt que le fuseau actuel. Dans ce cas, fournissez une sélection de fuseau via l’interface.

Questions Fréquentes

Quelle est la différence entre ZonedDateTime et OffsetDateTime ?

ZonedDateTime contient un identifiant de fuseau régional (ex., « Europe/Moscow ») et gère le DST. OffsetDateTime stocke seulement un décalage fixe (+03:00) sans règles régionales. Pour le stockage en base de données, OffsetDateTime est recommandé.

Comment obtenir l’heure actuelle en UTC via ZonedDateTime ?

Utilisez ZonedDateTime.now(ZoneOffset.UTC) ou Instant.now().atZone(ZoneOffset.UTC). Les deux options retournent l’instant actuel avec un décalage nul. Pour un horodatage simple, utilisez Instant.now() sans liaison de fuseau.

Peut-on sérialiser ZonedDateTime via Gson ou Moshi ?

Oui, mais un adaptateur personnalisé est requis. Gson ne prend pas en charge ZonedDateTime par défaut. Moshi le prend en charge via Rfc3339DateJsonAdapter. Il est recommandé d’utiliser Kotlinx Serialization ou la bibliothèque JavaTimeModule pour Jackson.

Comment gérer la situation où l’heure tombe dans un trou DST ?

java.time décale automatiquement l’heure vers l’avant du montant du décalage. Par exemple, si 02h30 n’existe pas lorsque les horloges sont avancées à 03h00, ZonedDateTime créera un objet à 03h30. Vous pouvez vérifier la présence d’un trou via ZoneRules.getTransition(instant).

Pourquoi ZonedDateTime n’est-il pas recommandé pour les bases de données SQL ?

JDBC 4.2 prend en charge OffsetDateTime mais pas ZonedDateTime directement. ZonedDateTime contient un fuseau régional qui n’a pas d’équivalent en SQL. Il est recommandé de stocker OffsetDateTime ou Instant, et de stocker le fuseau dans une colonne séparée.

Résumé

  • ZonedDateTime est une classe immuable pour la date et l’heure avec fuseau horaire, gérant correctement le DST via la base de données IANA Time Zone Database.
  • La principale différence avec LocalDateTime est la présence d’un fuseau, faisant de ZonedDateTime un identifiant univoque d’un instant dans le temps.
  • Pour la conversion entre fuseaux, utilisez withZoneSameInstant(), qui préserve l’instant, pas withZoneSameLocal.
  • Lors des transitions d’heure d’été, java.time résout automatiquement les trous et chevauchements via des règles de fuseau intégrées.
  • Pour le stockage en base de données, utilisez OffsetDateTime ou stockez Instant et ZoneId séparément.
  • Sous Android, pour convertir ZonedDateTime en heure locale de l’appareil, utilisez ZoneId.systemDefault() avec withZoneSameInstant.
  • Pour la sérialisation JSON, un adaptateur personnalisé est requis — utilisez Kotlinx Serialization ou Jackson JavaTimeModule.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi