Trabajando con fechas y horas (java.time)
Introducción
El manejo de fechas y horas es una tarea común en la programación, ya sea para registrar eventos, calcular duraciones, mostrar calendarios o coordinar actividades en diferentes zonas horarias. Antes de Java 8, las clases para manipular fechas y horas (Date
, Calendar
y SimpleDateFormat
) presentaban diversos problemas: eran difíciles de usar, propensas a errores y no eran thread-safe.
Con la llegada de Java 8, se introdujo el paquete java.time
, basado en el proyecto Joda-Time, que proporciona un API completo, coherente y mucho más intuitivo para trabajar con fechas y horas. Este nuevo API resuelve los problemas de sus predecesores y añade numerosas funcionalidades para manipular diferentes aspectos temporales.
En este artículo, exploraremos las principales clases del paquete java.time
y aprenderemos a utilizarlas para resolver problemas comunes relacionados con fechas y horas en nuestras aplicaciones Java.
Clases principales del paquete java.time
El paquete java.time
está compuesto por varias clases, cada una diseñada para un propósito específico:
LocalDate
Representa una fecha sin hora ni zona horaria, como "2023-07-15".
import java.time.LocalDate;
public class EjemploLocalDate {
public static void main(String[] args) {
// Fecha actual
LocalDate hoy = LocalDate.now();
System.out.println("Hoy es: " + hoy);
// Crear una fecha específica
LocalDate fechaNacimiento = LocalDate.of(1990, 5, 15);
System.out.println("Fecha de nacimiento: " + fechaNacimiento);
// Analizar una fecha desde una cadena
LocalDate fechaDesdeTexto = LocalDate.parse("2023-12-31");
System.out.println("Fecha desde texto: " + fechaDesdeTexto);
// Obtener componentes
System.out.println("Año: " + hoy.getYear());
System.out.println("Mes (número): " + hoy.getMonthValue());
System.out.println("Mes (nombre): " + hoy.getMonth());
System.out.println("Día del mes: " + hoy.getDayOfMonth());
System.out.println("Día de la semana: " + hoy.getDayOfWeek());
System.out.println("Día del año: " + hoy.getDayOfYear());
// Comprobar si es año bisiesto
System.out.println("¿Es año bisiesto? " + hoy.isLeapYear());
}
}
LocalTime
Representa una hora sin fecha ni zona horaria, como "15:30:45".
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
public class EjemploLocalTime {
public static void main(String[] args) {
// Hora actual
LocalTime ahora = LocalTime.now();
System.out.println("Hora actual: " + ahora);
// Crear una hora específica
LocalTime horaCafe = LocalTime.of(16, 30);
System.out.println("Hora del café: " + horaCafe);
// Analizar una hora desde una cadena
LocalTime horaDesdeTexto = LocalTime.parse("08:45:30");
System.out.println("Hora desde texto: " + horaDesdeTexto);
// Obtener componentes
System.out.println("Hora: " + ahora.getHour());
System.out.println("Minutos: " + ahora.getMinute());
System.out.println("Segundos: " + ahora.getSecond());
System.out.println("Nanosegundos: " + ahora.getNano());
// Manipular horas
LocalTime masTarde = ahora.plusHours(2);
System.out.println("Dos horas más tarde: " + masTarde);
LocalTime masTemp = ahora.minusMinutes(30);
System.out.println("30 minutos antes: " + masTemp);
// Truncar a la unidad especificada
LocalTime horaTruncada = ahora.truncatedTo(ChronoUnit.MINUTES);
System.out.println("Hora truncada a minutos: " + horaTruncada);
}
}
LocalDateTime
Combina fecha y hora sin zona horaria, como "2023-07-15T15:30:45".
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
public class EjemploLocalDateTime {
public static void main(String[] args) {
// Fecha y hora actual
LocalDateTime ahora = LocalDateTime.now();
System.out.println("Fecha y hora actual: " + ahora);
// Crear a partir de componentes
LocalDateTime reunion = LocalDateTime.of(2023, Month.JULY, 20, 14, 30);
System.out.println("Reunión programada: " + reunion);
// Crear combinando LocalDate y LocalTime
LocalDate fecha = LocalDate.of(2023, 12, 24);
LocalTime hora = LocalTime.of(20, 0);
LocalDateTime nochebuena = LocalDateTime.of(fecha, hora);
System.out.println("Nochebuena: " + nochebuena);
// Analizar desde una cadena
LocalDateTime desdeTexto = LocalDateTime.parse("2023-01-01T00:00:00");
System.out.println("Año nuevo: " + desdeTexto);
// Manipulación de fecha y hora
LocalDateTime futuro = ahora.plusDays(7).plusHours(3);
System.out.println("Una semana y 3 horas después: " + futuro);
// Extraer componentes
LocalDate soloFecha = ahora.toLocalDate();
LocalTime soloHora = ahora.toLocalTime();
System.out.println("Solo fecha: " + soloFecha);
System.out.println("Solo hora: " + soloHora);
}
}
ZonedDateTime
Representa una fecha y hora con zona horaria, útil para operaciones que dependen de la zona geográfica.
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class EjemploZonedDateTime {
public static void main(String[] args) {
// Fecha y hora actual con zona horaria del sistema
ZonedDateTime ahoraLocal = ZonedDateTime.now();
System.out.println("Ahora local: " + ahoraLocal);
// Fecha y hora en una zona específica
ZonedDateTime ahoraTokio = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("Ahora en Tokio: " + ahoraTokio);
// Listar todas las zonas horarias disponibles
System.out.println("\nAlgunas zonas horarias disponibles:");
ZoneId.getAvailableZoneIds().stream()
.filter(zona -> zona.startsWith("Europe"))
.sorted()
.limit(5)
.forEach(System.out::println);
// Convertir entre zonas horarias
ZonedDateTime madrid = ahoraLocal.withZoneSameInstant(ZoneId.of("Europe/Madrid"));
System.out.println("\nMisma hora en Madrid: " + madrid);
// Comprobar si una zona está adelantada respecto a otra
boolean madridAdelantada = madrid.isAfter(ahoraTokio);
System.out.println("¿Madrid está adelantada respecto a Tokio? " + madridAdelantada);
}
}
Instant
Representa un momento preciso en el tiempo, similar a los milisegundos desde la época UNIX (1970-01-01T00:00:00Z).
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class EjemploInstant {
public static void main(String[] args) {
// Instante actual
Instant ahora = Instant.now();
System.out.println("Instante actual: " + ahora);
// Creación desde epoch (segundos desde 1970-01-01T00:00:00Z)
Instant epoch = Instant.ofEpochSecond(0);
System.out.println("Epoch: " + epoch);
// Operaciones con instantes
Instant futuro = ahora.plus(1, ChronoUnit.DAYS);
System.out.println("Mañana a esta hora: " + futuro);
// Calcular duración entre instantes
long segundosEntreMediciones = ChronoUnit.SECONDS.between(epoch, ahora);
System.out.println("Segundos desde 1970: " + segundosEntreMediciones);
// Uso práctico: medir el tiempo de ejecución
Instant inicio = Instant.now();
// Simulamos un proceso que tarda un poco
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instant fin = Instant.now();
long duracionMs = ChronoUnit.MILLIS.between(inicio, fin);
System.out.println("Duración del proceso: " + duracionMs + " ms");
}
}
Duraciones y periodos
Java time proporciona clases para representar intervalos de tiempo:
Duration
Representa una cantidad de tiempo en términos de segundos y nanosegundos.
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
public class EjemploDuration {
public static void main(String[] args) {
// Crear duración directamente
Duration dosHoras = Duration.ofHours(2);
System.out.println("Dos horas: " + dosHoras);
// Duración entre dos instantes
Instant inicio = Instant.now();
// Simulamos un proceso
Instant fin = inicio.plusMillis(5325);
Duration duracion = Duration.between(inicio, fin);
System.out.println("Duración: " + duracion);
System.out.println("Duración en milisegundos: " + duracion.toMillis());
// Duración entre horas
LocalTime inicioClase = LocalTime.of(9, 0);
LocalTime finClase = LocalTime.of(10, 30);
Duration tiempoClase = Duration.between(inicioClase, finClase);
System.out.println("Tiempo de clase: " + tiempoClase);
System.out.println("Minutos de clase: " + tiempoClase.toMinutes());
// Operaciones aritméticas con duraciones
Duration descanso = Duration.ofMinutes(15);
Duration tiempoTotal = tiempoClase.plus(descanso);
System.out.println("Tiempo total con descanso: " + tiempoTotal);
}
}
Period
Representa una cantidad de tiempo en términos de años, meses y días.
import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
public class EjemploPeriod {
public static void main(String[] args) {
// Crear un periodo directamente
Period tresYDos = Period.of(3, 2, 15); // 3 años, 2 meses y 15 días
System.out.println("Periodo: " + tresYDos);
// Periodo entre dos fechas
LocalDate fechaInicio = LocalDate.of(1990, Month.JANUARY, 1);
LocalDate fechaFin = LocalDate.of(1990, Month.APRIL, 15);
Period periodo = Period.between(fechaInicio, fechaFin);
System.out.println("Periodo entre fechas: " + periodo);
// Calcular edad en años, meses y días
LocalDate fechaNacimiento = LocalDate.of(1990, 5, 15);
LocalDate hoy = LocalDate.now();
Period edad = Period.between(fechaNacimiento, hoy);
System.out.println("Edad: " + edad.getYears() + " años, " +
edad.getMonths() + " meses y " +
edad.getDays() + " días");
// Normalizar un periodo (convertir meses y días excesivos)
Period unNormalizado = Period.of(1, 15, 0); // 1 año y 15 meses
// Java no normaliza automáticamente, pero podemos hacerlo manualmente
int totalMeses = unNormalizado.getYears() * 12 + unNormalizado.getMonths();
int años = totalMeses / 12;
int meses = totalMeses % 12;
System.out.println("Normalizado: " + años + " años y " + meses + " meses");
}
}
Formateo y análisis de fechas y horas
La clase DateTimeFormatter
permite convertir objetos de fecha y hora a cadenas y viceversa, con gran flexibilidad para personalizar el formato.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class EjemploDateTimeFormatter {
public static void main(String[] args) {
LocalDateTime ahora = LocalDateTime.now();
// Formateos predefinidos
DateTimeFormatter formateadorCorto = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
System.out.println("Formato corto: " + ahora.format(formateadorCorto));
DateTimeFormatter formateadorMedio = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
System.out.println("Formato medio: " + ahora.format(formateadorMedio));
// Formato personalizado
DateTimeFormatter personalizado = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
System.out.println("Formato personalizado: " + ahora.format(personalizado));
// Localización (internacionalización)
DateTimeFormatter formateadorES =
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(new Locale("es", "ES"));
System.out.println("Formato español: " + ahora.format(formateadorES));
// Analizar (parsear) una cadena a fecha
String fechaTexto = "15/07/2023";
DateTimeFormatter analizador = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate fecha = LocalDate.parse(fechaTexto, analizador);
System.out.println("Fecha analizada: " + fecha);
}
}
Ajustadores temporales
Los ajustadores temporales (TemporalAdjusters
) permiten realizar operaciones más complejas con fechas, como encontrar el primer día del mes, el último día del año, etc.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class EjemploTemporalAdjusters {
public static void main(String[] args) {
LocalDate hoy = LocalDate.now();
System.out.println("Fecha actual: " + hoy);
// Primer día del mes
LocalDate primerDiaMes = hoy.with(TemporalAdjusters.firstDayOfMonth());
System.out.println("Primer día del mes: " + primerDiaMes);
// Último día del mes
LocalDate ultimoDiaMes = hoy.with(TemporalAdjusters.lastDayOfMonth());
System.out.println("Último día del mes: " + ultimoDiaMes);
// Próximo lunes
LocalDate proximoLunes = hoy.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
System.out.println("Próximo lunes: " + proximoLunes);
// Día de la semana en el mes (por ejemplo, el tercer jueves del mes)
LocalDate tercerJueves = hoy.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.THURSDAY));
System.out.println("Tercer jueves del mes: " + tercerJueves);
// Primer día del próximo año
LocalDate primerDiaProximoAño = hoy.with(TemporalAdjusters.firstDayOfNextYear());
System.out.println("Primer día del próximo año: " + primerDiaProximoAño);
}
}
Cronómetros y medición de tiempo
Para medir intervalos de tiempo de manera precisa, podemos usar Instant
junto con Duration
:
import java.time.Duration;
import java.time.Instant;
public class EjemploCronometro {
public static void main(String[] args) {
// Cronometrar el tiempo de ejecución de un algoritmo
Instant inicio = Instant.now();
// Simulamos un algoritmo que tarda un tiempo
fibonacci(40);
Instant fin = Instant.now();
Duration tiempo = Duration.between(inicio, fin);
System.out.println("Tiempo de ejecución: " + tiempo.toMillis() + " ms");
}
// Método de Fibonacci (ineficiente a propósito para la demostración)
private static long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Casos de uso prácticos
Calculadora de edades
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
public class CalculadoraEdad {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Calculadora de edad");
System.out.println("------------------");
System.out.print("Ingresa tu fecha de nacimiento (dd/MM/yyyy): ");
String fechaNacimientoStr = scanner.nextLine();
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate fechaNacimiento = LocalDate.parse(fechaNacimientoStr, formatter);
LocalDate fechaActual = LocalDate.now();
if (fechaNacimiento.isAfter(fechaActual)) {
System.out.println("Error: La fecha de nacimiento no puede ser futura");
return;
}
Period edad = Period.between(fechaNacimiento, fechaActual);
System.out.println("\nTu edad es:");
System.out.println(edad.getYears() + " años, " +
edad.getMonths() + " meses y " +
edad.getDays() + " días");
LocalDate proximoCumpleaños = fechaNacimiento
.withYear(fechaActual.getYear());
// Si ya pasó el cumpleaños este año, calculamos para el siguiente
if (proximoCumpleaños.isBefore(fechaActual) || proximoCumpleaños.isEqual(fechaActual)) {
proximoCumpleaños = proximoCumpleaños.plusYears(1);
}
Period hastaProximoCumpleaños = Period.between(fechaActual, proximoCumpleaños);
System.out.println("\nTiempo hasta tu próximo cumpleaños:");
System.out.println(hastaProximoCumpleaños.getMonths() + " meses y " +
hastaProximoCumpleaños.getDays() + " días");
} catch (Exception e) {
System.out.println("Error: Formato de fecha incorrecto. Usa dd/MM/yyyy");
} finally {
scanner.close();
}
}
}
Planificador de eventos
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
class Evento {
private String nombre;
private LocalDate fecha;
private LocalTime hora;
public Evento(String nombre, LocalDate fecha, LocalTime hora) {
this.nombre = nombre;
this.fecha = fecha;
this.hora = hora;
}
@Override
public String toString() {
DateTimeFormatter formateadorFecha = DateTimeFormatter.ofPattern("dd/MM/yyyy");
DateTimeFormatter formateadorHora = DateTimeFormatter.ofPattern("HH:mm");
return nombre + " - " + fecha.format(formateadorFecha) +
" a las " + hora.format(formateadorHora);
}
public LocalDate getFecha() {
return fecha;
}
}
public class PlanificadorEventos {
public static void main(String[] args) {
List<Evento> eventos = new ArrayList<>();
// Fecha actual
LocalDate hoy = LocalDate.now();
// Crear algunos eventos
eventos.add(new Evento("Reunión de equipo",
hoy.with(TemporalAdjusters.next(DayOfWeek.MONDAY)),
LocalTime.of(10, 0)));
eventos.add(new Evento("Entrega del proyecto",
hoy.plusWeeks(2),
LocalTime.of(18, 0)));
eventos.add(new Evento("Revisión de código",
hoy.plusDays(2),
LocalTime.of(14, 30)));
eventos.add(new Evento("Presentación al cliente",
hoy.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth()),
LocalTime.of(9, 0)));
// Mostrar todos los eventos
System.out.println("Lista de eventos:");
eventos.forEach(System.out::println);
// Filtrar eventos de la próxima semana
LocalDate inicioSemanaSiguiente = hoy.plusDays(7 - hoy.getDayOfWeek().getValue() + 1);
LocalDate finSemanaSiguiente = inicioSemanaSiguiente.plusDays(6);
System.out.println("\nEventos de la próxima semana:");
eventos.stream()
.filter(e -> !e.getFecha().isBefore(inicioSemanaSiguiente) &&
!e.getFecha().isAfter(finSemanaSiguiente))
.forEach(System.out::println);
}
}
Recomendaciones para trabajar con fechas y horas
-
Utiliza las clases adecuadas:
LocalDateTime
para fecha y hora sin zona horariaZonedDateTime
cuando necesites zona horariaInstant
para timestamps precisosLocalDate
solo para fechasLocalTime
solo para horas
-
Inmutabilidad: Recuerda que todas las clases del paquete
java.time
son inmutables. Cada operación devuelve un nuevo objeto, no modifica el original:
LocalDate fecha = LocalDate.now();
// Incorrecto: no cambia fecha
fecha.plusDays(1);
System.out.println(fecha); // Sigue siendo la fecha original
// Correcto: asignar el resultado a una variable
LocalDate mañana = fecha.plusDays(1);
System.out.println(mañana); // Ahora sí es un día después
-
Zonas horarias: Sé explícito con las zonas horarias cuando trabajes con aplicaciones distribuidas o eventos que ocurren en diferentes lugares.
-
Formateo: Utiliza
DateTimeFormatter
para formatear fechas y horas de manera personalizada, especialmente al mostrarlas a los usuarios o guardarlas en bases de datos.
Resumen
El paquete java.time
proporciona un API completo y robusto para trabajar con fechas, horas y duraciones en Java. A diferencia de las antiguas clases Date
y Calendar
, estas nuevas clases son inmutables, thread-safe y mucho más intuitivas de usar.
En este artículo hemos explorado las principales clases como LocalDate
, LocalTime
, LocalDateTime
, ZonedDateTime
e Instant
, así como Duration
y Period
para manejar intervalos de tiempo. También hemos visto cómo formatear y analizar fechas con DateTimeFormatter
y cómo aplicar ajustes complejos con TemporalAdjusters
.
Dominar estas herramientas te permitirá implementar correctamente funcionalidades relacionadas con el tiempo en tus aplicaciones Java, como calculadoras de edad, planificadores de eventos, mediciones de rendimiento y cualquier operación que requiera precisión temporal. En el próximo artículo, exploraremos el poder de las expresiones regulares en Java, otra herramienta imprescindible para la manipulación y validación de texto.