ZonedDateTime — qué es, cómo trabajar con zonas horarias y fechas

Autor: IT Sectr Publicado: 2026-07-13 Tiempo de lectura: 10 min

ZonedDateTime es una clase inmutable del paquete java.time que almacena fecha y hora junto con información de zona horaria (ZoneId). A diferencia de LocalDateTime, ZonedDateTime identifica de manera inequívoca un momento en la línea de tiempo. Según la especificación de Oracle Java 17 (2024), la clase maneja correctamente los cambios de horario de verano (DST) a través de las reglas de zona de la base de datos IANA Time Zone Database.

Puntos Clave

  • ZonedDateTime es una clase inmutable que combina fecha, hora y zona horaria (ZoneId) en un solo objeto.
  • A diferencia de LocalDateTime, ZonedDateTime define de manera inequívoca un momento en la línea de tiempo y es adecuado para sistemas globales.
  • La clase maneja automáticamente los cambios de horario de verano (DST) según las reglas de la base de datos IANA Time Zone Database.
  • Para convertir entre zonas horarias, se utiliza el método withZoneSameInstant(ZoneId).
  • Se recomienda almacenar ZonedDateTime en bases de datos mediante OffsetDateTime o TIMESTAMP WITH TIME ZONE.

¿Qué es ZonedDateTime?

ZonedDateTime es una de las clases clave del paquete java.time, que representa fecha y hora con información completa de zona horaria. Combina tres componentes: LocalDateTime (fecha y hora), ZoneId (identificador de zona) y ZoneOffset (desplazamiento relativo a UTC).

A diferencia de LocalDateTime, que almacena solo la hora de pared (wall-clock time) sin vinculación a zona, ZonedDateTime identifica de manera inequívoca un momento. Dos instancias idénticas de LocalDateTime en diferentes zonas horarias representan momentos distintos. Dos instancias idénticas de ZonedDateTime — el mismo momento.

La clase es completamente inmutable y thread-safe. Todas las operaciones aritméticas devuelven un nuevo objeto. ZonedDateTime implementa la interfaz ChronoZonedDateTime y puede usarse donde sea necesario trabajar con tiempo zonal en Java.

Según las especificaciones de Oracle Java 17, ZonedDateTime admite trabajar con cualquier zona de la base de datos IANA Time Zone Database, que incluye más de 600 zonas horarias.

ZonedDateTime vs LocalDateTime: ¿cuál es la diferencia?

La diferencia principal — ZonedDateTime contiene una zona horaria, mientras que LocalDateTime no. Esta diferencia fundamental determina el ámbito de cada clase.

LocalDateTime se usa para eventos locales: hora de un concierto, horario de clases, fecha de nacimiento. Si un evento ocurre en Moscú a las 15:00, LocalDateTime registrará las 15:00 sin vinculación. Si mueves el servidor a Nueva York, la hora seguirá siendo 15:00 — pero será un momento físico diferente.

ZonedDateTime se usa para datos globales: registros del servidor, marcas de tiempo en API, reuniones internacionales. Si una reunión está programada a las 15:00 MSK, ZonedDateTime conservará tanto la hora como la zona. En Nueva York, se mostrará correctamente como las 8:00 EST. Según Baeldung (2024), elegir entre LocalDateTime y ZonedDateTime es la decisión arquitectónica más común al trabajar con fechas.

Regla práctica: si los datos se almacenan para una sola región — usa LocalDateTime. Si los datos cruzan límites de zonas horarias — usa ZonedDateTime. Si necesitas representar un momento absoluto — usa Instant.

¿Cómo funciona la zona horaria en java.time?

La zona horaria en java.time está representada por la clase ZoneId. ZoneId es un identificador de zona en el formato “continente/región”, por ejemplo “Europe/Moscow”, “America/New_York”, “Asia/Tokyo”. ZoneId se obtiene mediante el método estático of(String zoneId) o a través de la zona horaria predeterminada del sistema.

ZoneId se divide en dos tipos: fixed offset (desplazamiento fijo, ej. “+03:00”) y region-based (zonas regionales, ej. “Europe/London”). Las zonas regionales contienen reglas de cambio de horario de verano y cambios históricos. Fixed offset es simplemente un desplazamiento fijo.

Para obtener el desplazamiento actual de un ZoneId en un momento específico, se usa el método getRules(), que devuelve ZoneRules. ZoneRules contiene todas las transiciones y desplazamientos para una zona determinada. Este es el mecanismo clave para el manejo correcto de DST.

Todas las zonas horarias se incluyen con el JDK a través de los archivos tzdata (IANA Time Zone Database) y se actualizan regularmente. En Android, la versión de tzdata depende de las actualizaciones del sistema a través de Google Play Services.

Creación de ZonedDateTime

ZonedDateTime se puede crear de varias formas. La más simple es now(), que devuelve la hora actual en la zona horaria del sistema. La variante now(ZoneId) permite obtener la hora actual en una zona especificada.

El método of(LocalDateTime, ZoneId) crea un ZonedDateTime a partir de hora local y zona. La variante of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) crea a partir de componentes.

LocalDateTime se puede convertir a ZonedDateTime mediante el método atZone(ZoneId). Instant — mediante Instant.atZone(ZoneId). Date — mediante 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)

Conversión entre zonas horarias

El método principal de conversión es withZoneSameInstant(ZoneId). Convierte un ZonedDateTime a otra zona horaria conservando el mismo momento. Por ejemplo, 15:00 MSK → 8:00 EST. El método withZoneSameLocal(ZoneId) cambia la zona conservando la hora local — esto produce un momento diferente.

Para obtener el desplazamiento relativo a UTC, se usa el método getOffset(), que devuelve ZoneOffset. ZoneOffset es una subclase de ZoneId que representa un desplazamiento fijo en el formato “+HH:mm” o “-HH:mm”.

La conversión a Instant se realiza mediante el método toInstant(). Instant es un momento absoluto en el tiempo, independiente de la zona horaria. La conversión inversa es 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"))

Trabajar con el horario de verano (DST)

Los cambios de horario de verano crean dos problemas: huecos (gaps) y superposiciones (overlaps). Un hueco ocurre en primavera cuando los relojes se adelantan — cierta hora no existe. Una superposición ocurre en otoño cuando los relojes se atrasan — la misma hora ocurre dos veces.

ZonedDateTime maneja estas situaciones mediante una estrategia de resolución. Al crear un objeto durante un hueco, java.time desplaza automáticamente la hora por la cantidad del desplazamiento. Al crear durante una superposición, se selecciona la primera opción (antes del cambio). Este comportamiento se puede cambiar mediante withZoneSameInstant.

Puedes verificar si una hora está en DST mediante zone.getRules().isDaylightSavings(instant). El método getOffset() muestra el desplazamiento real para un momento dado, y getRules().getDaylightSavings(instant) muestra el ajuste por DST en milisegundos.

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("Horario de Verano", "Compensación horario de verano: $dstAmount")
    }
}

Formateo de ZonedDateTime

Para formatear ZonedDateTime se usa DateTimeFormatter. El formato ISO estándar incluye fecha, hora y desplazamiento: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. Formatos predefinidos: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Para salida localizada, usa DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle puede ser SHORT, MEDIUM, LONG, FULL. LONG incluye el nombre de la zona (“MSK”), FULL incluye el nombre completo (“Moscow Standard Time”).

Importante: al analizar una cadena con ZonedDateTime, el formato debe contener información de zona o desplazamiento. Si no se especifica la zona, usa LocalDateTime.parse() y luego 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 en Android: ejemplos prácticos

El primer ejemplo — mostrar la hora de una reunión para el usuario en su zona horaria local. El servidor envía ZonedDateTime en UTC, el cliente convierte a la zona horaria local del dispositivo.

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)
}

El segundo ejemplo — calcular el tiempo hasta el próximo evento considerando la zona horaria. Usamos ZonedDateTime para la hora del servidor y Duration.between() para calcular la diferencia.

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"
}

El tercer ejemplo — trabajar con la API de Retrofit. El servidor devuelve una cadena ISO-8601 con zona. Usamos un deserializador personalizado para convertir a 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()
        )
    }
}

Errores comunes al trabajar con zonas horarias

El primer error — usar ZoneId.systemDefault() en el código del servidor. La zona horaria del servidor puede diferir de la del cliente, y usar la zona del sistema en el servidor lleva a cálculos incorrectos. Siempre especifica la zona explícitamente o usa UTC como referencia.

El segundo error — ignorar el DST al calcular la duración. Duration.between() maneja correctamente las transiciones, pero si restas marcas de tiempo manualmente, el horario de verano puede causar un error de 1 hora. Usa ChronoUnit.HOURS.between() en lugar de cálculos manuales.

El tercer error — confundir withZoneSameInstant y withZoneSameLocal. El primero cambia la zona conservando el momento — la hora se desplaza. El segundo cambia la zona conservando la hora local — el momento cambia. Elegir el método incorrecto es uno de los errores más comunes según SonarSource (2024).

El cuarto error — asumir que la zona horaria del dispositivo es siempre la misma que la zona horaria del usuario. El usuario puede estar viajando y esperar que la aplicación muestre la hora en su zona “de origen” en lugar de la actual. En este caso, proporciona selección de zona a través de la interfaz.

Preguntas Frecuentes

¿Cuál es la diferencia entre ZonedDateTime y OffsetDateTime?

ZonedDateTime contiene un identificador de zona regional (ej., “Europe/Moscow”) y maneja DST. OffsetDateTime almacena solo un desplazamiento fijo (+03:00) sin reglas regionales. Para almacenamiento en base de datos, se recomienda OffsetDateTime.

¿Cómo obtener la hora actual en UTC mediante ZonedDateTime?

Usa ZonedDateTime.now(ZoneOffset.UTC) o Instant.now().atZone(ZoneOffset.UTC). Ambas opciones devuelven el momento actual con desplazamiento cero. Para una marca de tiempo simple, usa Instant.now() sin vinculación de zona.

¿Se puede serializar ZonedDateTime con Gson o Moshi?

Sí, pero se requiere un adaptador personalizado. Gson no admite ZonedDateTime por defecto. Moshi lo admite mediante Rfc3339DateJsonAdapter. Se recomienda usar Kotlinx Serialization o la biblioteca JavaTimeModule para Jackson.

¿Cómo manejar la situación cuando la hora cae en un hueco de DST?

java.time desplaza automáticamente la hora hacia adelante por la cantidad del desplazamiento. Por ejemplo, si las 02:30 no existen cuando los relojes se adelantan a las 03:00, ZonedDateTime creará un objeto a las 03:30. Puedes verificar si hay un hueco mediante ZoneRules.getTransition(instant).

¿Por qué no se recomienda ZonedDateTime para bases de datos SQL?

JDBC 4.2 admite OffsetDateTime pero no ZonedDateTime directamente. ZonedDateTime contiene una zona regional que no tiene equivalente en SQL. Se recomienda almacenar OffsetDateTime o Instant, y almacenar la zona en una columna separada.

Resumen

  • ZonedDateTime es una clase inmutable para fecha y hora con zona horaria, que maneja correctamente DST mediante la base de datos IANA Time Zone Database.
  • La diferencia principal con LocalDateTime es la presencia de una zona, lo que convierte a ZonedDateTime en un identificador inequívoco de un momento en el tiempo.
  • Para conversión entre zonas, usa withZoneSameInstant(), que conserva el momento, no withZoneSameLocal.
  • Durante los cambios de horario de verano, java.time resuelve automáticamente huecos y superposiciones mediante reglas de zona integradas.
  • Para almacenamiento en base de datos, usa OffsetDateTime o almacena Instant y ZoneId por separado.
  • En Android, para convertir ZonedDateTime a la hora local del dispositivo, usa ZoneId.systemDefault() junto con withZoneSameInstant.
  • Para serialización JSON, se requiere un adaptador personalizado — usa Kotlinx Serialization o Jackson JavaTimeModule.

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también