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:
- Cada tipo es inmutable. Un
LocalDateuna vez creado nunca cambia. Métodos comoplusDays(7)devuelven un nuevoLocalDate. Esto hace que la API sea segura para hilos por diseño y elimina toda una categoría de errores. - Cada tipo significa una sola cosa.
LocalDatees una fecha sin hora.Instantes un momento en la línea de tiempo.Durationes una longitud de tiempo. ElDateheredado 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 BerlinLa jerarquía de zonas:
ZoneOffsetes un desplazamiento fijo±HH:MMrespecto a UTC:+05:30,-08:00. Sin manejo de DST.ZoneIdes 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í.
Lo que se puede extraer de la ejecución:
- El primer bloque construyó la familia por composición:
LocalDate+LocalTime=LocalDateTime;LocalDateTime+ZoneId=ZonedDateTime;ZonedDateTime→Instant. 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)yZonedDateTime.toLocalDateTime()cierran los bucles. - Un mismo
Instantmostró tres horas de aspecto diferente vistas desde Nueva York, Berlín y Tokio. Ese es el punto deInstant: es el momento, independiente de dónde estés parado. ElZonedDateTimeañade la etiqueta de "dónde estoy parado". Confundir ambos es el error delDateheredado. Durationse imprimió comoPT1H30MyPeriodcomoP3M. El formato de duración ISO-8601 esPnYnMnDTnHnMnS— lo que va antes de laTson unidades de calendario (Period), lo que va después son unidades de tiempo (Duration). El string es exactamente lo que devuelvetoString(), y exactamente lo que aceptaparse(...).today.plusDays(7)produjo unLocalDatediferente. Imprimirtodayde nuevo justo después mostró que el original no había cambiado — esa es la garantía de inmutabilidad. Cadaplus/minus/withdevuelve 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 unlong, no unPeriod, 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 usaChronoUniten 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.