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:
- Cada tipo es inmutable. Una
LocalDateya creada nunca cambia. Métodos comoplusDays(7)devuelven una nuevaLocalDate. Eso hace la API segura para hilos por construcción y elimina toda una categoría de errores. - Cada tipo significa una cosa.
LocalDatees una fecha sin hora.Instantes un momento en la línea de tiempo.Durationes una longitud de tiempo. La antiguaDateera 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 BerlinLa jerarquía de zonas:
ZoneOffsetes un desfase fijo±HH:MMrespecto a UTC:+05:30,-08:00. Sin manejo del horario de verano.ZoneIdes 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í.
Qué llevarse de la ejecución:
- El primer bloque construyó la familia por composición:
LocalDate+LocalTime=LocalDateTime;LocalDateTime+ZoneId=ZonedDateTime;ZonedDateTime→Instant. 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)yZonedDateTime.toLocalDateTime()cierran los bucles. - Un solo
Instantimprimió tres horas de «aspecto» distinto vistas desde Nueva York, Berlín y Tokio. Ese es el sentido deInstant: es el momento, independiente de dónde esté usted parado. ElZonedDateTimeañade la etiqueta de «dónde estoy parado». Confundir los dos es el error de la antiguaDate. Durationse imprimió comoPT1H30MyPeriodcomoP3M. El formato de duración ISO-8601 esPnYnMnDTnHnMnS— todo lo anterior a laTson unidades de calendario (Period), todo lo posterior son unidades de tiempo (Duration). La cadena es literalmente lo que devuelvetoString(), y literalmente lo que aceptaparse(...).today.plusDays(7)produjo unaLocalDatedistinta. Imprimirtodayde nuevo justo después mostró que el original no cambió — esa es la garantía de inmutabilidad. Cadaplus/minus/withdevuelve 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 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 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
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?