UD2 · Sección 6 — Fechas y tiempos: java.time
| Dato | Valor |
|---|---|
| Unidad Didáctica | UD2 — Programación Orientada a Objetos con la API de Java |
| Resultado de Aprendizaje | RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos. |
| Sección | 6 de 10 — Fechas y tiempos: java.time |
| Evaluación | 1.ª Evaluación |
| Duración estimada | 3 horas lectivas |
| Criterios de evaluación cubiertos | CE 2.2 (escribe programas simples), CE 2.3 (instancia objetos a partir de clases predefinidas), CE 2.4 (utiliza métodos y propiedades), CE 2.5 (llamadas a métodos estáticos), CE 2.6 (utiliza parámetros), CE 2.7 (incorpora y utiliza librerías de objetos), CE 2.8 (utiliza constructores) |
| Prerrequisitos | UD1 completa, UD2 Secciones 1 a 5 |
| Paquete del código de ejemplo | dam.programacion.ud2 |
| Herramientas | JDK 25, IntelliJ IDEA Community, Maven 3.9.x |
1. Punto de partida
Trabajar con fechas y horas parece trivial hasta que te enfrentas a ello. ¿Cuántos días hay entre el 1 de enero y el 15 de marzo? ¿Y si hay un año bisiesto por medio? ¿En qué cambia la hora de Madrid respecto a Tokio? ¿Y respecto a la hora UTC? ¿Cuántos segundos tiene un mes de febrero? Si has intentado responder alguna de estas preguntas a mano, sabes que la cosa se complica enseguida.
Históricamente, Java ofrecía las clases Date y Calendar (en java.util), que eran notoriamente difíciles de usar: meses numerados desde 0, mutabilidad no esperada, zonas horarias mal gestionadas, API confusa. En Java 8 (2014) se introdujo el paquete java.time, basado en la librería Joda-Time, que resolvió todos esos problemas. Desde entonces, Date y Calendar se consideran obsoletos para nuevo código: los seguirás viendo en proyectos viejos, pero tú no deberías usarlos.
En esta sección vas a conocer las cinco clases principales de java.time para representar momentos (LocalDate, LocalTime, LocalDateTime, ZonedDateTime), los dos tipos para intervalos (Period y Duration), y la clase para formatear y parsear (DateTimeFormatter). Al terminar, serás capaz de representar cualquier fecha u hora, calcular diferencias y formatearlas para mostrarlas al usuario.
2. Por qué java.time reemplaza a Date y Calendar
Antes de entrar en la nueva API, conviene entender por qué se considera que Date y Calendar están "deprecados" para nuevo código. Esto te ayudará a apreciar las decisiones de diseño de java.time y a no caer en los mismos errores si los ves en código heredado.
Problema con Date / Calendar |
Cómo lo resuelve java.time |
|---|---|
Date es mutable: un método puede modificar tu fecha sin que te enteres |
Todas las clases de java.time son inmutables |
Calendar numera los meses desde 0 (enero = 0, diciembre = 11) |
java.time usa Month.JANUARY hasta Month.DECEMBER (1 a 12) |
Date representa un instante concreto, no "una fecha" abstracta |
LocalDate representa solo una fecha, sin hora ni zona |
Los años en Date se cuentan desde 1900 (getYear() devuelve año - 1900) |
LocalDate.getYear() devuelve el año completo (2026) |
| La API mezcla conceptos (fecha, hora, instante, zona) en una sola clase | java.time separa conceptos en clases distintas |
SimpleDateFormat no es seguro en concurrencia |
DateTimeFormatter es inmutable y seguro |
Regla práctica: en código nuevo, usa siempre java.time. Si te encuentras Date o Calendar en un proyecto viejo, conviértelos a java.time tan pronto como puedas (tienes métodos como Date.toInstant() y Date.from(Instant) para hacer el puente).
3. Las cinco clases principales
java.time separa los conceptos en clases distintas, cada una con una misión clara. Esa separación es la clave de su claridad.
graph TD
A["java.time"]
A --> LD["LocalDate<br/>fecha sin hora"]
A --> LT["LocalTime<br/>hora sin fecha"]
A --> LDT["LocalDateTime<br/>fecha y hora"]
A --> ZDT["ZonedDateTime<br/>fecha, hora y zona"]
A --> I["Instant<br/>instante en UTC"]
LD -->|"combina con"| LT
LD --> LDT
LT --> LDT
LDT -->|"añade zona"| ZDT
I -.se asocia a.-> ZDT
style A fill:#1565c0,color:#fff,stroke:#0d47a1
style LD fill:#2e7d32,color:#fff,stroke:#1b5e20
style LT fill:#2e7d32,color:#fff,stroke:#1b5e20
style LDT fill:#ef6c00,color:#fff,stroke:#e65100
style ZDT fill:#6a1b9a,color:#fff,stroke:#4a148c
style I fill:#c62828,color:#fff,stroke:#b71c1c
| Clase | Qué representa | Ejemplo |
|---|---|---|
LocalDate |
Una fecha sin hora ni zona (14/07/2026) | Tu cumpleaños |
LocalTime |
Una hora sin fecha ni zona (17:30) | La hora a la que suena tu alarma |
LocalDateTime |
Fecha + hora, sin zona (14/07/2026 17:30) | La hora de inicio de un evento local |
ZonedDateTime |
Fecha + hora + zona horaria (14/07/2026 17:30 Europe/Madrid) | Una reunión internacional |
Instant |
Un instante preciso en UTC (linea temporal universal) | Marca de tiempo de un log |
Analogía de la cita. Imagina que quedas con un amigo para tomar un café:
LocalDate: "Quedamos el 14 de julio" (sin hora, sin ciudad).LocalTime: "Quedamos a las 17:30" (sin fecha, sin ciudad).LocalDateTime: "Quedamos el 14 de julio a las 17:30" (sin ciudad).ZonedDateTime: "Quedamos el 14 de julio a las 17:30 en Madrid" (con todo). Si tu amigo vive en México, sabrá exactamente a qué hora local le corresponde.Instant: "Quedamos en el segundo 1.783.642.000 del reloj UTC" (lo que usan los ordenadores entre ellos).
Para la mayoría de aplicaciones de escritorio o de un solo país, LocalDate y LocalDateTime son suficientes. Para aplicaciones internacionales o servidores, conviene usar ZonedDateTime o Instant.
En esta sección nos centraremos en las tres primeras (LocalDate, LocalTime, LocalDateTime) y presentaremos ZonedDateTime e Instant brevemente.
4. LocalDate: fecha sin hora
LocalDate representa una fecha: año, mes y día, sin hora y sin zona horaria. Es la clase que más usarás para representar cumpleaños, fechas de vencimiento, días festivos, etc.
4.1. Crear un LocalDate
Hay tres formas principales:
Trampa de los meses. A diferencia del viejo Calendar (que numeraba enero como 0), LocalDate.of(2026, 7, 14) significa 14 de julio. Los meses van de 1 a 12. Para mayor claridad, usa la enumeración Month.JANUARY...Month.DECEMBER.
4.2. Métodos principales
| Método | Qué hace | Devuelve |
|---|---|---|
now() |
Fecha actual (método estático) | LocalDate |
of(int año, int mes, int dia) |
Crea una fecha concreta (estático) | LocalDate |
parse(String) |
Parsea en formato ISO (estático) | LocalDate |
getYear() |
Año | int |
getMonthValue() |
Mes (1-12) | int |
getMonth() |
Mes como enumeración | Month |
getDayOfMonth() |
Día del mes | int |
getDayOfWeek() |
Día de la semana | DayOfWeek |
getDayOfYear() |
Día del año (1-366) | int |
isLeapYear() |
¿Es año bisiesto? | boolean |
lengthOfMonth() |
Días del mes | int |
lengthOfYear() |
Días del año | int |
plusDays(long) |
Suma días | LocalDate |
plusMonths(long) |
Suma meses | LocalDate |
plusYears(long) |
Suma años | LocalDate |
plusWeeks(long) |
Suma semanas | LocalDate |
minusDays(long) |
Resta días | LocalDate |
minusMonths(long) |
Resta meses | LocalDate |
minusYears(long) |
Resta años | LocalDate |
isBefore(LocalDate) |
¿Es anterior a otra? | boolean |
isAfter(LocalDate) |
¿Es posterior a otra? | boolean |
isEqual(LocalDate) |
¿Es igual a otra? | boolean |
Inmutabilidad. Como en String, los métodos plus* y minus* no modifican el objeto original: devuelven un LocalDate nuevo. Si olvidas asignar el resultado, no pasa nada.
4.3. Ejemplo práctico
Nota sobre withYear. Es un método que devuelve un LocalDate con el año cambiado pero conservando mes y día. Lo verás más en el ejemplo integrador; la idea general es que los métodos with* permiten "ajustar" un campo concreto.
5. LocalTime: hora sin fecha
LocalTime representa una hora del día: horas, minutos, segundos y nanosegundos, sin fecha ni zona. Útil para representar horas de apertura, alarmas, duraciones puntuales.
5.1. Crear un LocalTime
5.2. Métodos principales
| Método | Qué hace |
|---|---|
now() |
Hora actual (estático) |
of(int h, int m) / of(int h, int m, int s) / of(int h, int m, int s, int ns) |
Crea hora concreta (estático) |
parse(String) |
Parsea en formato ISO "HH:mm" o "HH:mm:ss" (estático) |
getHour(), getMinute(), getSecond(), getNano() |
Componentes |
plusHours(long), plusMinutes(long), plusSeconds(long) |
Sumar |
minusHours(long), minusMinutes(long), minusSeconds(long) |
Restar |
isBefore(LocalTime), isAfter(LocalTime) |
Comparación |
5.3. Ejemplo
Duration lo veremos en la sección 8; el ejemplo te adelanta su uso más común.
6. LocalDateTime: fecha + hora
LocalDateTime combina LocalDate y LocalTime: tiene año, mes, día, hora, minuto, segundo y nanosegundo, pero sin zona horaria. Es lo que usarás cuando necesites representar "el momento exacto de un evento local" sin preocuparte de zonas horarias.
6.1. Crear un LocalDateTime
Los métodos atTime y atDate son muy expresivos: "esta fecha, a esta hora" o "esta hora, en esta fecha".
6.2. Métodos principales
Los mismos de LocalDate y LocalTime combinados: getYear, getMonthValue, getDayOfMonth, getHour, getMinute, getSecond, plusDays, plusHours, minusMonths, etc.
7. ZonedDateTime e Instant (introducción)
7.1. ZonedDateTime
Añade la zona horaria a un LocalDateTime. Necesario cuando trabajas con eventos internacionales o quieres evitar ambigüedades con cambios de hora (verano/invierno).
Como ves, aunque la hora local en Madrid y Tokio es distinta, representan el mismo instante universal: cuando en Madrid son las 17:30 del 14 de julio, en Tokio son las 00:30 del 15 de julio.
7.2. Instant
Instant representa un instante preciso en la línea temporal UTC, con precisión de nanosegundo. Es lo que usan los sistemas internamente para marcar tiempos (logs, base de datos, eventos).
Diferencia clave: Instant no tiene zona horaria (es UTC implícito), ZonedDateTime sí la tiene. Instant es para máquinas; ZonedDateTime es para humanos.
7.3. Cuándo usar cada uno
| Necesidad | Clase |
|---|---|
| Cumpleaños, festivos, vencimientos | LocalDate |
| Horas de apertura, alarmas | LocalTime |
| Eventos locales sin zona | LocalDateTime |
| Reuniones internacionales, reservas de vuelo | ZonedDateTime |
| Marcas de tiempo en logs, base de datos | Instant |
8. Period y Duration: intervalos de tiempo
java.time distingue dos tipos de intervalos:
Period: intervalo en términos de años, meses y días. Apropiado para diferencias entre fechas (humanas).Duration: intervalo en términos de horas, minutos, segundos y nanosegundos. Apropiado para diferencias entre instantes (mecánicas).
8.1. Period
8.2. Duration
8.3. Diferencia entre Period y Duration
| Aspecto | Period |
Duration |
|---|---|---|
| Unidades | Años, meses, días | Horas, minutos, segundos, nanos |
| Basado en | Campos calendáricos (humanos) | Tiempo absoluto (segundos) |
| Apropiado para | "Tengo 21 años" | "El turno dura 8 horas" |
between(a, b) |
Entre dos LocalDate (o LocalDateTime) |
Entre dos LocalTime (o Instant) |
Sumar a LocalDate |
Sí, con plus(Period) |
No |
Sumar a LocalTime |
No | Sí, con plus(Duration) |
Trampa. Duration entre dos LocalDateTime calcula la diferencia absoluta en segundos, sin tener en cuenta cambios de horario de verano/invierno. Period entre dos LocalDate sí respeta el calendario (un mes es un mes, tenga 28 o 31 días). Por eso:
- Para "cuántos años/cumpleaños", usa
Period. - Para "cuántas horas falta", usa
Duration.
9. DateTimeFormatter: formatear y parsear
Hasta ahora, al imprimir las fechas hemos visto el formato ISO (2026-07-14T17:30:45). Pero los humanos solemos preferir formatos más naturales como "14/07/2026" o "14 de julio de 2026". Para personalizar el formato, se usa DateTimeFormatter.
9.1. Formateadores predefinidos
java.time.format.DateTimeFormatter ofrece algunos formateadores ya listos:
| Formateador | Formato | Ejemplo |
|---|---|---|
ISO_LOCAL_DATE |
yyyy-MM-dd |
2026-07-14 |
ISO_LOCAL_TIME |
HH:mm:ss |
17:30:45 |
ISO_LOCAL_DATE_TIME |
yyyy-MM-dd'T'HH:mm:ss |
2026-07-14T17:30:45 |
ofLocalizedDate(...) |
Formato localizado de fecha | 14 jul 2026 |
ofLocalizedTime(...) |
Formato localizado de hora | 17:30 |
ofLocalizedDateTime(...) |
Formato localizado de fecha+hora | 14-jul-2026 17:30 |
9.2. Formateadores personalizados
Para crear tu propio formato, usa patrones con letras especiales:
9.3. Patrones más comunes
| Letra | Significado | Ejemplo |
|---|---|---|
yyyy |
Año (4 dígitos) | 2026 |
yy |
Año (2 dígitos) | 26 |
MM |
Mes (2 dígitos, 01-12) | 07 |
MMM |
Mes abreviado | jul |
MMMM |
Mes completo | julio |
dd |
Día del mes (2 dígitos) | 14 |
d |
Día del mes (sin ceros) | 14 |
EEEE |
Día de la semana completo | martes |
EEE |
Día de la semana abreviado | mar |
HH |
Hora (24h, 00-23) | 17 |
hh |
Hora (12h, 01-12) | 05 |
mm |
Minutos | 30 |
ss |
Segundos | 45 |
a |
AM/PM | PM |
9.4. Parsear con un formato personalizado
Si el texto no encaja con el patrón, se lanza DateTimeParseException. Más adelante, en UD3, verás cómo gestionar estas excepciones con try-catch.
9.5. Localización
Los nombres de meses y días salen en el idioma por defecto de la JVM. Para forzar un idioma concreto:
10. Ejemplo integrador
Programa que combina los conceptos de la sección. Simula una pequeña gestión de eventos: crea un evento, calcula días hasta el evento, calcula edad, gestiona horarios y formatea todo de forma amigable.
Salida (parcial; la fecha actual y los cálculos relativos dependen del momento de ejecución):
11. Errores comunes
| Error | Qué pasa | Cómo evitarlo |
|---|---|---|
Olvidar el import |
"cannot find symbol" | Añade los imports correspondientes (java.time.LocalDate, etc.) |
Esperar que plus* modifique el objeto original |
El cambio se pierde | Asigna el resultado: fecha = fecha.plusDays(1); |
Confundir Period y Duration |
Resultados inesperados al calcular intervalos | Period para años/meses/días entre fechas; Duration para horas/minutos/segundos entre instantes |
Usar Date o Calendar en código nuevo |
API confusa y propensa a errores | Usa siempre java.time |
Olvidar el Locale al formatear en español |
Los nombres salen en el idioma por defecto de la JVM | Pasa new Locale("es", "ES") al DateTimeFormatter |
Confundir MMM con MM |
MM es numérico, MMM es texto |
Revisa la tabla de patrones |
Pasar fechas en formato no ISO a parse(String) sin DateTimeFormatter |
DateTimeParseException |
Usa parse(String, DateTimeFormatter) para formatos personalizados |
Usar LocalDateTime para eventos internacionales |
Errores con cambios horarios (verano/invierno) | Usa ZonedDateTime o Instant |
| Pensar que los meses van de 0 a 11 | Esa trampa es del viejo Calendar |
En java.time, los meses van de 1 a 12 |
Olvidar el manejo de DateTimeParseException al parsear entrada de usuario |
El programa falla si el formato es incorrecto | Usa try-catch (lo verás en UD3) |
12. Resumen
| Concepto | Idea clave |
|---|---|
LocalDate |
Fecha sin hora ni zona (14/07/2026) |
LocalTime |
Hora sin fecha ni zona (17:30) |
LocalDateTime |
Fecha + hora sin zona |
ZonedDateTime |
Fecha + hora + zona horaria |
Instant |
Instante preciso en UTC (para máquinas) |
Period |
Intervalo en años/meses/días |
Duration |
Intervalo en horas/minutos/segundos |
DateTimeFormatter |
Formatea y parsea fechas con patrones |
| Inmutabilidad | Todos los métodos devuelven nuevas instancias |
now() |
Método estático para obtener el momento actual |
of(...) |
Método estático para crear valores concretos |
parse(String) / parse(String, formatter) |
Parsea desde texto |
plus* / minus* / with* |
Operaciones que devuelven nuevos objetos |
isBefore / isAfter / isEqual |
Comparación |
Month / DayOfWeek |
Enumeraciones legibles para meses y días |
Locale |
Para controlar el idioma de nombres de meses/días |
13. Cuestionario de autoevaluación
Preguntas
-
Nombra dos problemas del viejo
Date/Calendarquejava.timeresuelve. -
¿Qué clase usarías para representar la fecha de tu cumpleaños? ¿Y la hora a la que suena tu alarma? ¿Y la fecha y hora exactas de tu próxima cita médica?
-
¿Es
LocalDatemutable o inmutable? Demuéstralo con un ejemplo de código. -
¿Qué devuelve
LocalDate.of(2026, 7, 14).getDayOfWeek()? ¿Qué tipo de dato devuelve? -
Escribe una línea que cree un
LocalDaterepresentando el 25 de diciembre de 2026 usando la enumeraciónMonth. -
¿Qué diferencia hay entre
PeriodyDuration? Pon un ejemplo de uso para cada uno. -
Escribe el patrón
DateTimeFormatterpara formatear una fecha como "14/07/2026". -
¿Qué patrón usarías para mostrar la fecha "martes, 14 de julio de 2026"?
-
¿Qué ocurre si llamas a
LocalDate.parse("14-07-2026")sin pasar unDateTimeFormatter? -
¿Por qué no es buena idea usar
LocalDateTimepara representar la hora de una reunión entre personas de distintos países? -
¿Cómo se obtiene la fecha y hora actuales en Madrid usando
ZonedDateTime? -
Si tienes un
LocalDate fechay escribesfecha.plusDays(5);sin asignar el resultado, ¿qué le pasa afecha? -
¿Qué clase usarías para almacenar la marca de tiempo de un log:
LocalDateTimeoInstant? ¿Por qué? -
¿Qué método de
LocalDatete dice cuántos días tiene el mes actual? -
¿Cómo harías para que los nombres de meses salieran en español al formatear una fecha?
Soluciones
-
Por ejemplo: (a)
Datees mutable, lo que permite que un método modifique tu fecha sin que te enteres;java.timees inmutable. (b)Calendarnumera los meses desde 0 (enero = 0), lo que genera bugs;java.timelos numera de 1 a 12 y además ofrece la enumeraciónMonth. -
Cumpleaños:
LocalDate(no necesita hora ni zona). Alarma:LocalTime(no necesita fecha ni zona). Cita médica:LocalDateTime(necesita fecha y hora, pero normalmente no es internacional; si lo fuera,ZonedDateTime). -
Es inmutable. Ejemplo:
Para que el cambio surta efecto hay que asignar:f = f.plusDays(1);. -
Devuelve un valor de tipo
DayOfWeek(enumeración), concretamenteDayOfWeek.TUESDAY(martes). Para obtener un número (1-7, donde lunes es 1), usaríasgetDayOfWeek().getValue(). -
LocalDate navidad = LocalDate.of(2026, Month.DECEMBER, 25); -
Periodexpresa intervalos en años, meses y días (apropiado para diferencias calendáricas entreLocalDate).Durationexpresa intervalos en horas, minutos, segundos y nanosegundos (apropiado para diferencias absolutas entre instantes u horas). Periodejemplo:Period.between(nacimiento, hoy)para calcular la edad.-
Durationejemplo:Duration.between(apertura, cierre)para calcular las horas de un turno. -
DateTimeFormatter.ofPattern("dd/MM/yyyy"). -
"EEEE, d 'de' MMMM 'de' yyyy"conLocaleespañol:DateTimeFormatter.ofPattern("EEEE, d 'de' MMMM 'de' yyyy", new Locale("es", "ES")). -
Se lanza
DateTimeParseExceptionporque el formato ISO por defecto deLocalDateesyyyy-MM-dd(con guiones en orden año-mes-día), nodd-MM-yyyy. Para parsear "14-07-2026" habría que crear unDateTimeFormattercon patrón"dd-MM-yyyy". -
Porque
LocalDateTimeno tiene información de zona horaria. Si una persona en Madrid y otra en México ven "reunión a las 17:00", cada una interpretará su hora local, y se conectarán a horas distintas.ZonedDateTimeelimina la ambigüedad indicando explícitamente la zona. -
ZonedDateTime ahoraMadrid = ZonedDateTime.now(ZoneId.of("Europe/Madrid")); -
Nada.
fechasigue teniendo su valor original porqueLocalDatees inmutable y el métodoplusDaysdevuelve un nuevoLocalDateque se descarta al no asignarlo. -
Instant, porque representa un instante preciso en UTC independiente de la zona horaria del servidor. Es lo que usan internamente los sistemas de logging y bases de datos para garantizar consistencia. -
lengthOfMonth(). -
Pasando un
Localeespañol alDateTimeFormatter:
Evaluación del cuestionario
| Aciertos | Nivel |
|---|---|
| 14-15 | Dominas la sección. Adelante con la siguiente. |
| 10-13 | Bien, repasa los puntos fallados. |
| 6-9 | Repasa la sección, especialmente la diferencia Period vs Duration y los patrones de formateo. |
| Menos de 6 | Vuelve a leer con calma y ejecuta el ejemplo integrador paso a paso en IntelliJ. |
14. Información ampliada y próximos pasos
- Javadoc oficial del paquete
java.time(Java 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/package-summary.html - Tutorial de Oracle sobre
java.time: https://docs.oracle.com/javase/tutorial/datetime/iso/index.html - Patrones de
DateTimeFormattercompletos: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/format/DateTimeFormatter.html - Artículo "Why java.time is better than Date/Calendar" (Baeldung): https://www.baeldung.com/java-8-date-time-intro
Próximos pasos:
- Practica en IntelliJ. Copia el ejemplo integrador, ejecútalo y modifícalo: cambia el evento por tu próximo examen, calcula los días que faltan, prueba a parsear tu fecha de nacimiento en formato
dd/MM/yyyyy a imprimirla con formato largo en español. - Experimenta con
PeriodyDuration. Calcula cuántos días has vivido y cuántas horas. Verás quePeriodte da años/meses/días yDurationte da horas absolutas. - Avanza a la Sección 7 de UD2.
StringBuildery concatenación eficiente: por qué concatenar strings con+en bucles es caro, cómoStringBuilderlo resuelve, y cómo usarStringJoineryString.joinpara unir elementos con separadores. Es una sección corta y muy práctica, especialmente útil para construir reportes y mensajes con muchos fragmentos.