Ir al contenido principal

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

  1. Utiliza las clases adecuadas:

    • LocalDateTime para fecha y hora sin zona horaria
    • ZonedDateTime cuando necesites zona horaria
    • Instant para timestamps precisos
    • LocalDate solo para fechas
    • LocalTime solo para horas
  2. 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
  1. Zonas horarias: Sé explícito con las zonas horarias cuando trabajes con aplicaciones distribuidas o eventos que ocurren en diferentes lugares.

  2. 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.