W3docs

Introducción a la API de fecha y hora de Java

Una introducción a la API moderna de fecha y hora de Java en java.time, que reemplaza a las antiguas clases Date y Calendar.

Introducción a la API de fecha y hora de Java

Java 8 añadió java.time, un nuevo paquete para representar fechas, horas, duraciones, zonas horarias y la aritmética entre ellas. Reemplazó a dos API anteriores — java.util.Date y java.util.Calendar — que tenían la merecida reputación de ser el rincón peor diseñado del JDK. La nueva API se inspiró en la anterior biblioteca Joda-Time de Stephen Colebourne; si ha usado Joda, java.time le resultará familiar.

Las dos cosas importantes del rediseño:

  1. Cada tipo es inmutable. Una LocalDate ya creada nunca cambia. Métodos como plusDays(7) devuelven una nueva LocalDate. Eso hace la API segura para hilos por construcción y elimina toda una categoría de errores.
  2. Cada tipo significa una cosa. LocalDate es una fecha sin hora. Instant es un momento en la línea de tiempo. Duration es una longitud de tiempo. La antigua Date era de algún modo todas estas a la vez, según qué constructor usara; la nueva API las separa para que el tipo le diga qué clase de valor tiene.

Este capítulo es el mapa. Los siguientes diez capítulos profundizan en cada clase.

Los tipos fundamentales

"A date"           LocalDate            2025-11-04
"A time of day"    LocalTime            14:30:00
"Both, no zone"    LocalDateTime        2025-11-04T14:30:00
"Both, with zone"  ZonedDateTime        2025-11-04T14:30:00-05:00 [America/New_York]
"A moment"         Instant              2025-11-04T19:30:00Z      (UTC, seconds-since-epoch)
"A length of time" Duration             PT1H30M                   (1 hour 30 minutes)
"A length of date" Period               P1Y2M3D                   (1 year 2 months 3 days)

La separación horizontal — Local* frente a Zoned/Instant — es la más importante. Los tipos Local no llevan zona horaria. Una LocalDate de 2025-11-04 es «el cuatro de noviembre»; no dice si es el cuatro en Tokio o en Honolulu. Es el tipo correcto para un cumpleaños, una fecha de contrato o un selector de fechas de la interfaz.

Los tipos Zoned llevan su zona. ZonedDateTime es «este instante de calendario en este lugar», que es lo que quiere para «reunión programada a las 9 de la mañana en Nueva York». Instant es un momento en la línea de tiempo global — segundos UTC desde la época — que es lo que quiere para el registro, marcas de tiempo de mensajes, cualquier cosa que deba ordenarse globalmente sin necesitar etiquetas locales.

La separación horizontal entre Duration y Period también importa. Duration es una longitud de tiempo que puede comparar en segundos — PT24H son exactamente 24 × 3600 segundos. Period es una longitud expresada en términos de calendario — P1M (un mes) son 30 días en algunos meses y 31 en otros. Para medir tiempos, quiere Duration. Para «sumar un mes a una fecha de facturación», quiere Period.

La forma fluida

Cada tipo se construye y modifica mediante una API fluida coherente:

LocalDate today    = LocalDate.now();
LocalDate stardate = LocalDate.of(2025, 11, 4);
LocalDate parsed   = LocalDate.parse("2025-11-04");

LocalDate nextWeek = today.plusDays(7);                     // immutable: returns a NEW LocalDate
LocalDate lastYear = today.minusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1);            // with* returns a copy with one field changed

boolean before = today.isBefore(stardate);
int year = today.getYear();

Tres formas que verá por todas partes:

  • now() — valor actual del reloj del sistema.
  • of(...) — componentes explícitos.
  • parse(...) — a partir de una cadena (ISO-8601 por defecto).

Y para las transformaciones:

  • plusX(n) / minusX(n) — aritmética.
  • withX(value) — reemplazar un único campo.
  • isBefore(other) / isAfter(other) — comparación.

Esta forma se repite a lo largo de LocalDate, LocalTime, LocalDateTime, ZonedDateTime e Instant. Una vez que conoce el patrón, cada clase le lee el mismo dialecto con un vocabulario ligeramente distinto.

Las zonas horarias son difíciles, y la API lo admite

La mayor razón por la que java.util.Date resultaba doloroso es que intentaba hacer invisibles las zonas horarias. El resultado fue la famosa clase de error «guarda un Date, recupéralo en un servidor en otra zona horaria, obtén la fecha de calendario equivocada». java.time lo resuelve haciendo explícita la zona en el tipo.

Si acepta una fecha de un usuario y no sabe en qué zona está, guárdela como LocalDate. Si le dice que son «las 9 de su hora» y conoce su zona, guárdela como ZonedDateTime con la zona. Si registra un evento del servidor, guárdelo como Instant. No guarde un LocalDateTime esperando que la zona horaria sobreviva; la zona faltante es todo el error.

Instant now = Instant.now();                                 // unambiguous: a moment in UTC
ZonedDateTime localized = now.atZone(ZoneId.of("Europe/Berlin"));  // a label for that moment in Berlin

La jerarquía de zonas:

  • ZoneOffset es un desfase fijo ±HH:MM respecto a UTC: +05:30, -08:00. Sin manejo del horario de verano.
  • ZoneId es una zona con nombre: Europe/Berlin, America/New_York. Lleva el registro de la base de datos IANA de qué desfase tiene esa zona en cualquier día dado, incluidas las transiciones de horario de verano y los cambios históricos.

Prefiera siempre ZoneId antes que ZoneOffset cuando pueda elegir. «America/New_York» es correcto a través del horario de verano; «−05:00» solo es correcto fuera del horario de verano.

Los tipos heredados no han desaparecido

java.util.Date, java.util.Calendar y java.text.SimpleDateFormat siguen existiendo. El código nuevo no debería usarlos — pero mucho código viejo sí, y necesitará interoperar. Los métodos de conversión son directos:

// java.util.Date <-> java.time.Instant
Instant inst = legacyDate.toInstant();
Date    back = Date.from(inst);

// java.util.Calendar -> java.time.ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(
    cal.toInstant(), cal.getTimeZone().toZoneId());

El patrón es unidireccional: heredado → java.time es sencillo; para todo lo nuevo, quédese en java.time y convierta solo en la frontera de la API donde vive el código viejo. Los capítulos Date heredada y Calendar al final de esta parte cubren el puente en detalle.

Un ejemplo resuelto: la familia de tipos en un solo programa

El programa de abajo usa cada tipo que el mapa anterior presentó — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period — y muestra cómo se convierten unos en otros. Es la versión «recorrido»; cada tipo individual recibe su propio capítulo a partir de aquí.

java— editable, runs on the server

Qué llevarse de la ejecución:

  • El primer bloque construyó la familia por composición: LocalDate + LocalTime = LocalDateTime; LocalDateTime + ZoneId = ZonedDateTime; ZonedDateTimeInstant. Esa es la retícula de conversiones, y las hará cada vez que cruce una frontera de API. Las flechas van en ambos sentidos para la mayoría de los pares — Instant.atZone(zone) y ZonedDateTime.toLocalDateTime() cierran los bucles.
  • Un solo Instant imprimió tres horas de «aspecto» distinto vistas desde Nueva York, Berlín y Tokio. Ese es el sentido de Instant: es el momento, independiente de dónde esté usted parado. El ZonedDateTime añade la etiqueta de «dónde estoy parado». Confundir los dos es el error de la antigua Date.
  • Duration se imprimió como PT1H30M y Period como P3M. El formato de duración ISO-8601 es PnYnMnDTnHnMnS — todo lo anterior a la T son unidades de calendario (Period), todo lo posterior son unidades de tiempo (Duration). La cadena es literalmente lo que devuelve toString(), y literalmente lo que acepta parse(...).
  • today.plusDays(7) produjo una LocalDate distinta. Imprimir today de nuevo justo después mostró que el original no cambió — esa es la garantía de inmutabilidad. Cada plus/minus/with devuelve un objeto nuevo; el receptor nunca se modifica. Sin copia defensiva, sin preocupaciones de seguridad de hilos, nunca.
  • ChronoUnit.DAYS.between(today, launch) fue la operación de «distancia». Devuelve un long, no un Period, porque la respuesta en días no tiene ambigüedad de calendario (a diferencia de los meses, que varían en longitud). Cada capítulo de esta parte usa ChronoUnit en algún punto — es el catálogo de unidades de tiempo del que habla la API.

Qué sigue

El siguiente capítulo, Java LocalDate, comienza el recorrido en profundidad. LocalDate es el más simple de los cinco tipos de «punto en el tiempo» y el lugar correcto para aprender la forma fluida que comparten todos los demás.

Practice

Práctica

You need to store the moment a server received an HTTP request, so that the log can be sorted globally across servers in different time zones. Which `java.time` type fits?