W3docs

Introducción a la API de fechas y horas en Java

Introducción a la moderna API java.time de Java 8, que reemplaza las clases Date y Calendar heredadas.

Java 8 añadió java.time, un nuevo paquete para representar fechas, horas, duraciones, zonas horarias y las operaciones aritméticas entre ellas. Reemplazó dos APIs anteriores — java.util.Date y java.util.Calendar — que tenían merecida fama de ser el rincón peor diseñado del JDK. La nueva API fue impulsada por la biblioteca Joda-Time de Stephen Colebourne; si has usado Joda, java.time te resultará familiar.

Los dos aspectos más importantes del rediseño:

  1. Cada tipo es inmutable. Un LocalDate una vez creado nunca cambia. Métodos como plusDays(7) devuelven un nuevo LocalDate. Esto hace que la API sea segura para hilos por diseño y elimina toda una categoría de errores.
  2. Cada tipo significa una sola cosa. LocalDate es una fecha sin hora. Instant es un momento en la línea de tiempo. Duration es una longitud de tiempo. El Date heredado era, de alguna manera, todas estas cosas a la vez, dependiendo del constructor que usaras; la nueva API las separa para que el tipo te indique qué clase de valor tienes.

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

Los tipos principales

"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 división horizontal — Local* frente a Zoned/Instant — es la más importante. Los tipos Local no llevan zona horaria. Un LocalDate de 2025-11-04 es "el cuatro de noviembre"; no indica si ese es el cuatro en Tokio o en Honolulu. Es el tipo adecuado para un cumpleaños, una fecha de contrato o un selector de fecha en la interfaz.

Los tipos con zona llevan su zona incorporada. ZonedDateTime es "este instante de calendario en este lugar", que es lo que necesitas para "reunión programada a las 9 AM en Nueva York." Instant es un momento en la línea de tiempo global — segundos UTC desde el epoch — que es lo que necesitas para registros, marcas de tiempo en mensajes, cualquier cosa que deba ordenarse globalmente sin necesitar etiquetas locales.

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

La forma fluida

Cada tipo se construye y modifica mediante una API fluida y 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ás en todas partes:

  • now() — valor actual del reloj del sistema.
  • of(...) — componentes explícitos.
  • parse(...) — a partir de un string (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 en LocalDate, LocalTime, LocalDateTime, ZonedDateTime e Instant. Una vez que conoces el patrón, cada clase te habla en el mismo dialecto con un vocabulario ligeramente distinto.

Las zonas horarias son complejas, y la API lo reconoce

La principal razón por la que java.util.Date era problemático es que intentaba hacer las zonas horarias invisibles. El resultado era la famosa clase de errores de "almacenas un Date, lo recuperas en un servidor en una zona horaria diferente y obtienes la fecha de calendario incorrecta." java.time resuelve esto haciendo la zona explícita en el tipo.

Si recibes una fecha de un usuario y no sabes en qué zona está, guárdala como LocalDate. Si te dice que es "a las 9 AM en su zona" y conoces su zona, guárdala como ZonedDateTime con la zona. Si registras un evento del servidor, guárdalo como Instant. No almacenes un LocalDateTime esperando que la zona horaria se transmita sola; la zona ausente es exactamente 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 desplazamiento fijo ±HH:MM respecto a UTC: +05:30, -08:00. Sin manejo de DST.
  • ZoneId es una zona con nombre: Europe/Berlin, America/New_York. Contiene el registro de la base de datos IANA sobre qué desplazamiento tiene esa zona en cualquier día dado, incluidas las transiciones de DST y los cambios históricos.

Siempre prefiere ZoneId sobre ZoneOffset cuando puedas elegir. "America/New_York" es correcto a lo largo del DST; "−05:00" solo es correcto fuera del DST.

Los tipos heredados siguen existiendo

java.util.Date, java.util.Calendar y java.text.SimpleDateFormat todavía existen. El código nuevo no debería usarlos — pero mucho código antiguo sí lo hace, y necesitarás 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 en una dirección: heredado → java.time es sencillo; para todo lo nuevo, quédate en java.time y convierte solo en el límite de la API donde vive el código antiguo. Los capítulos Legacy Date y Calendar al final de esta parte cubren el puente en detalle.

Un ejemplo práctico: la familia de tipos en un programa

El programa a continuación usa todos los tipos que el mapa anterior presentó — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period — y muestra cómo se convierten entre sí. Es la versión "tour"; cada tipo individual tiene su propio capítulo a partir de aquí.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • El primer bloque construyó la familia por composición: LocalDate + LocalTime = LocalDateTime; LocalDateTime + ZoneId = ZonedDateTime; ZonedDateTimeInstant. Esa es la red de conversiones, y la usarás cada vez que cruces un límite de API. Las flechas van en ambas direcciones para la mayoría de los pares — Instant.atZone(zone) y ZonedDateTime.toLocalDateTime() cierran los bucles.
  • Un mismo Instant mostró tres horas de aspecto diferente vistas desde Nueva York, Berlín y Tokio. Ese es el punto de Instant: es el momento, independiente de dónde estés parado. El ZonedDateTime añade la etiqueta de "dónde estoy parado". Confundir ambos es el error del Date heredado.
  • Duration se imprimió como PT1H30M y Period como P3M. El formato de duración ISO-8601 es PnYnMnDTnHnMnS — lo que va antes de la T son unidades de calendario (Period), lo que va después son unidades de tiempo (Duration). El string es exactamente lo que devuelve toString(), y exactamente lo que acepta parse(...).
  • today.plusDays(7) produjo un LocalDate diferente. Imprimir today de nuevo justo después mostró que el original no había cambiado — esa es la garantía de inmutabilidad. Cada plus/minus/with devuelve un nuevo objeto; el receptor nunca se modifica. Sin copias defensivas, sin preocupaciones de seguridad para 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 lugar — es el catálogo de unidades de tiempo sobre las que habla la API.

Qué sigue

El próximo capítulo, Java LocalDate, comienza el tour en profundidad. LocalDate es el más sencillo de los cinco tipos "punto en el tiempo" y el lugar adecuado para aprender la forma fluida que comparten todos los demás.

Práctica

Práctica
Necesitas almacenar el momento en que un servidor recibió una solicitud HTTP, para que el registro pueda ordenarse globalmente entre servidores en distintas zonas horarias. ¿Qué tipo de `java.time` es el adecuado?
Necesitas almacenar el momento en que un servidor recibió una solicitud HTTP, para que el registro pueda ordenarse globalmente entre servidores en distintas zonas horarias. ¿Qué tipo de `java.time` es el adecuado?
Was this page helpful?