W3docs

Java ZonedDateTime

Representa fechas y horas con zona horaria en Java usando ZonedDateTime y la clase ZoneId.

ZonedDateTime es un LocalDateTime con un ZoneId adjunto. Dice: "esta fecha y hora del calendario, en este lugar." La combinación identifica un único momento en la línea de tiempo global — 2025-11-04T14:00 [America/New_York] es exactamente un Instant, distinto de 2025-11-04T14:00 [Europe/Berlin].

Esta es la clase a la que se recurre siempre que importa la hora local del reloj de pared de un evento en un lugar específico. Calendarios de reuniones. Trabajos programados de tipo cron que deben ejecutarse a las "9 AM en la zona del usuario." Cualquier cosa que deba sobrevivir una transición de horario de verano (DST). LocalDateTime no sabe suficiente; Instant está en UTC y no lleva la etiqueta de zona significativa para el ser humano. ZonedDateTime es ambas cosas.

ZoneId: el catálogo de zonas

Antes de ZonedDateTime, hay que conocer el propio ZoneId — una zona se identifica mediante un ZoneId, que se obtiene con 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();

Las cadenas son identificadores de la base de datos de zonas horarias IANA (Región/Ciudad). La lista completa es ZoneId.getAvailableZoneIds() — alrededor de 600 entradas, actualizadas periódicamente cuando los países cambian sus reglas de zona o DST. ZoneId lleva el registro histórico de IANA, por lo que las fechas de 1985 usan las reglas que estaban en vigor en 1985.

Evita ZoneOffset (un ±HH:MM fijo) cuando te refieras a una zona real. ZoneOffset.of("-05:00") es correcto para Nueva York en noviembre y erróneo en junio; ZoneId.of("America/New_York") es correcto durante todo el año.

Los nombres de zona de tres letras como "EST" y "PST" son en su mayoría alias ahora, ambiguos (¿era Eastern Standard o Eastern Australia?), y están silenciosamente en desuso. Usa Región/Ciudad. "UTC" y "GMT" son casos especiales y están bien.

Creación

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]");

El camino de construcción más común es "tengo un LocalDateTime, tengo un ZoneId, los adjunto":

LocalDateTime ldt = LocalDateTime.of(2025, 11, 4, 14, 0);
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));

atZone(zone) es el puente de una sola llamada desde una lectura del reloj local hasta un momento con zona. También maneja los dos casos extremos que introduce el DST.

DST: cuando el reloj de pared salta o se repite

Dos veces al año, el reloj de pared en cualquier zona que observe el DST salta o se repite. Cuando adelanta la hora — en EE. UU., las 02:00 saltan a las 03:00 un domingo de marzo — los tiempos entre las 02:00 y las 03:00 no existen ese día. Cuando retrocede, los tiempos entre la 01:00 y las 02:00 ocurren dos veces. ZonedDateTime tiene que hacer algo en ambos casos, y lo que hace está documentado:

  • Tiempo omitido (hueco): atZone devuelve el tiempo posterior a la transición. LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York")) se convierte en 03:30-04:00 — el JDK avanzó una hora para aterrizar en un reloj de pared válido.
  • Tiempo repetido (superposición): atZone devuelve el primero de los dos momentos válidos (el anterior al cambio de offset). Usa withEarlierOffsetAtOverlap() o withLaterOffsetAtOverlap() para elegir explícitamente.
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)

Los dos ZonedDateTimes tienen el mismo LocalDateTime pero diferentes offsets y diferentes Instants. Este es el único lugar en java.time donde la misma lectura del reloj local se mapea legítimamente a dos momentos — y es la fuente de los errores relacionados con DST que habrás oído. Sé deliberado cuando la superposición importa.

Descomposición

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();

Los accesores se dividen en tres grupos: la mitad de zona (getZone, getOffset), la mitad del reloj local (toLocalDateTime, toLocalDate, toLocalTime), y la mitad del momento global (toInstant). Los tres son simultáneamente verdaderos del mismo ZonedDateTime; eliges la proyección que necesitas.

OffsetDateTime es un tipo relacionado — LocalDateTime más un ZoneOffset (sin zona, sin DST). Es útil para serializar "2025-11-04T14:00-05:00" sin comprometerse a una zona con nombre (que es a menudo lo que quieren las marcas de tiempo JSON); para cualquier código que necesite aritmética consciente del DST, conserva el ZonedDateTime.

Dos tipos de "día siguiente"

ZonedDateTime tiene dos métodos que parecen similares y no lo son:

zdt.plusDays(1);                                              // add 1 day to the local clock reading
zdt.plus(Duration.ofHours(24));                                // add exactly 24 hours

En un día de transición DST, los dos divergen. En el día que los relojes adelantan la hora, plusDays(1) aterriza en la misma hora local del día siguiente (que está a solo 23 horas de tiempo real). plus(Duration.ofHours(24)) aterriza en una hora del reloj de pared una hora más tarde que la del día anterior.

ObjetivoMétodo
"La misma hora mañana" (calendario)plusDays(1)
"Exactamente 24 horas desde ahora" (duración)plus(Duration.ofHours(24))

Ambos son correctos; responden preguntas diferentes. Elige con deliberación.

Comparaciones e igualdad

zdt1.isBefore(zdt2);                                          // compares Instants
zdt1.isAfter(zdt2);
zdt1.isEqual(zdt2);                                           // compares Instants
zdt1.equals(zdt2);                                            // compares LocalDateTime + Zone + Offset

La distinción es clara:

  • isBefore/isAfter/isEqual comparan los momentos subyacentes (Instants).
  • equals compara la estructura completa — dos ZonedDateTimes que representan el mismo momento pero tienen zonas diferentes no son equal.

Para "¿son estos el mismo momento independientemente de la zona?", usa isEqual o convierte ambos a Instant y compara.

Un ejemplo práctico: una reunión en tres oficinas

El programa a continuación programa una reunión para las 14:00 hora de Berlín y calcula a qué hora corresponde en las oficinas de Nueva York y Tokio. Luego programa una reunión semanal recurrente que sobrevive una transición DST, demostrando la diferencia entre plusDays(7) y plus(Duration.ofDays(7)) en una semana de transición.

java— editable, runs on the server

Lo que se aprende de la ejecución:

  • withZoneSameInstant(otherZone) es la operación para "¿qué hora es en su oficina?" — mantiene el momento fijo y vuelve a mostrar el reloj de pared en la nueva zona. Su hermano withZoneSameLocal(otherZone) mantiene el reloj de pared y cambia el momento (la reunión se mueve). Los nombres son confundibles; la diferencia es qué cosa permanece igual. Léelos con cuidado cuando los escribas.
  • berlin.equals(ny) fue false aunque los dos representaban el mismo momento. equals compara la estructura completa (fecha-hora local + zona). Para "mismo momento independientemente de cómo esté etiquetado," usa isEqual o compara Instants. Es exactamente la misma distinción que LocalDate.equals vs isEqualequals para "mismo objeto-valor," isEqual para "mismo punto en el tiempo."
  • El hueco DST (2025-03-09 02:30 en NY) se resolvió avanzando a 03:30-04:00. El JDK no lanzó una excepción; eligió el momento posterior a la transición. Si necesitas detectar absolutamente que suministraste una hora imposible, usa ZoneRules.getTransition(localDateTime) y comprueba si el objeto devuelto es un hueco.
  • La superposición DST (2025-11-02 01:30 en NY) te dio dos ZonedDateTimes distintos con los mismos campos locales y diferentes offsets — EDT vs EST, separados por una hora. withLaterOffsetAtOverlap() y withEarlierOffsetAtOverlap() son la forma de elegir. Si almacenas eventos programados, decide de antemano cuál de los dos quiere el usuario y aplica la llamada correcta en el momento del análisis.
  • plusDays(1) y plus(Duration.ofHours(24)) produjeron resultados diferentes en el día del adelanto de hora — 23 horas de tiempo real frente a 24 horas, aterrizando en diferentes horas del reloj. Usa plusDays/plusWeeks para la programación en forma de calendario ("la misma hora mañana") y plus(Duration) para la aritmética de tiempo transcurrido ("alarma en 24 horas"). La elección casi siempre refleja la intención del usuario.

Qué sigue

ZonedDateTime es el lado amigable para el ser humano de "un momento con una etiqueta." El siguiente capítulo, Java Instant, es el lado amigable para la máquina — un momento como nanosegundos desde la época, sin zona, sin calendario, el tipo que todo sistema distribuido usa en el cable.

Práctica

Práctica
Programas una reunión recurrente a las 09:00 America/New_York y almacenas la siguiente ocurrencia con `nextMeeting.plusDays(7)`. En la semana que cruza la transición DST de adelanto de hora, ¿qué es cierto del resultado?
Programas una reunión recurrente a las 09:00 America/New_York y almacenas la siguiente ocurrencia con `nextMeeting.plusDays(7)`. En la semana que cruza la transición DST de adelanto de hora, ¿qué es cierto del resultado?
Was this page helpful?