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):
atZonedevuelve el tiempo posterior a la transición.LocalDateTime.of(2025, 3, 9, 2, 30).atZone(ZoneId.of("America/New_York"))se convierte en03:30-04:00— el JDK avanzó una hora para aterrizar en un reloj de pared válido. - Tiempo repetido (superposición):
atZonedevuelve el primero de los dos momentos válidos (el anterior al cambio de offset). UsawithEarlierOffsetAtOverlap()owithLaterOffsetAtOverlap()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 hoursEn 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.
| Objetivo | Mé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 + OffsetLa distinción es clara:
isBefore/isAfter/isEqualcomparan los momentos subyacentes (Instants).equalscompara la estructura completa — dosZonedDateTimes que representan el mismo momento pero tienen zonas diferentes no sonequal.
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.
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 hermanowithZoneSameLocal(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)fuefalseaunque los dos representaban el mismo momento.equalscompara la estructura completa (fecha-hora local + zona). Para "mismo momento independientemente de cómo esté etiquetado," usaisEqualo comparaInstants. Es exactamente la misma distinción queLocalDate.equalsvsisEqual—equalspara "mismo objeto-valor,"isEqualpara "mismo punto en el tiempo."- El hueco DST (
2025-03-09 02:30en NY) se resolvió avanzando a03: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, usaZoneRules.getTransition(localDateTime)y comprueba si el objeto devuelto es un hueco. - La superposición DST (
2025-11-02 01:30en NY) te dio dosZonedDateTimes distintos con los mismos campos locales y diferentes offsets —EDTvsEST, separados por una hora.withLaterOffsetAtOverlap()ywithEarlierOffsetAtOverlap()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)yplus(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. UsaplusDays/plusWeekspara la programación en forma de calendario ("la misma hora mañana") yplus(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.