Zademy

Expresiones Lambda en Java: Guía Esencial para Recorridos y Fechas

Java; Lambdas; Stream; Fechas; Tutorial
words palabras

Java 8 introdujo lambdas y, sinceramente, cambió cómo escribo Java. El código pasó de ser verboso y torpe a algo mucho más compacto y expresivo, sobre todo al trabajar con colecciones y fechas. Este apunte recorre los patrones que uso a diario.

Qué son las Expresiones Lambda

Una lambda es una función anónima que puedes pasar como argumento. Tratas comportamiento como si fuera dato. En lugar de crear una clase anónima de seis líneas para un callback de una sola instrucción, escribes una línea.

Sintaxis Básica

Tres partes: parámetros, flecha, cuerpo.

(parámetros) -> { cuerpo de la función }

Ejemplo simple:

// Forma tradicional con clase anónima
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hola Mundo");
    }
};

// Forma con lambda (más concisa)
Runnable r2 = () -> System.out.println("Hola Mundo");

Recorrido de Colecciones con Lambdas

Recorrer Arrays

Con Arrays.stream y forEach queda bastante limpio:

String[] nombres = {"Ana", "Carlos", "Beatriz", "David"};

// Recorrido con forEach y lambda
Arrays.stream(nombres)
      .forEach(nombre -> System.out.println("Hola " + nombre));

// O con referencia a método (method reference)
Arrays.stream(nombres)
      .forEach(System.out::println);

Recorrer Listas

Las listas son donde más se nota la diferencia. forEach directo sobre la lista, sin iteradores, sin índices manuales:

List<String> frutas = Arrays.asList("Manzana", "Banana", "Naranja", "Fresa");

// Recorrido básico
frutas.forEach(fruta -> System.out.println("Me gusta la " + fruta));

// Recorrido con números de posición
IntStream.range(0, frutas.size())
         .forEach(i -> System.out.println(i + ": " + frutas.get(i)));

// Filtrar y recorrer
frutas.stream()
      .filter(fruta -> fruta.length() > 6)
      .forEach(fruta -> System.out.println("Fruta larga: " + fruta));

Recorrer Mapas

Los mapas tienen su propio forEach que recibe clave y valor. Tres formas de recorrerlos, según qué necesites:

Map<String, Integer> edades = new HashMap<>();
edades.put("Ana", 25);
edades.put("Carlos", 32);
edades.put("Beatriz", 28);

// Recorrer entry set (clave-valor)
edades.forEach((nombre, edad) ->
    System.out.println(nombre + " tiene " + edad + " años")
);

// Recorrer solo las claves
edades.keySet().forEach(clave ->
    System.out.println("Persona: " + clave)
);

// Recorrer solo los valores
edades.values().forEach(valor ->
    System.out.println("Edad: " + valor)
);

Manejo de Fechas con Lambdas

La API de fechas de Java 8 (java.time) y las lambdas se llevan bien. Filtrar, ordenar y formatear fechas queda bastante natural con streams.

Trabajar con LocalDate

LocalDate es una fecha sin hora. Directo al grano:

// Crear fechas
LocalDate hoy = LocalDate.now();
LocalDate navidad = LocalDate.of(2025, 12, 25);

// Operaciones con fechas usando streams
List<LocalDate> fechasImportantes = Arrays.asList(
    LocalDate.of(2025, 1, 1),
    LocalDate.of(2025, 5, 10),
    LocalDate.of(2025, 9, 16),
    LocalDate.of(2025, 12, 25)
);

// Filtrar fechas futuras
fechasImportantes.stream()
    .filter(fecha -> fecha.isAfter(hoy))
    .forEach(fecha -> System.out.println("Fecha futura: " + fecha));

// Formatear fechas
fechasImportantes.forEach(fecha ->
    System.out.println(fecha.format(DateTimeFormatter.ofPattern("dd 'de' MMMM 'de' yyyy")))
);

Trabajar con LocalTime

LocalTime es la hora sin fecha. Útil para horarios:

// Crear horas
LocalTime ahora = LocalTime.now();
LocalTime almuerzo = LocalTime.of(14, 30);

// Lista de horas del día
List<LocalTime> horas = Arrays.asList(
    LocalTime.of(8, 0),   // Inicio de trabajo
    LocalTime.of(12, 0),  // Almuerzo
    LocalTime.of(14, 30),  // Regreso de almuerzo
    LocalTime.of(18, 0)   // Fin de trabajo
);

// Filtrar horas laborales
horas.stream()
    .filter(hora -> hora.isBefore(almuerzo))
    .forEach(hora -> System.out.println("Mañana: " + hora));

// Convertir a minutos del día
horas.forEach(hora ->
    System.out.println("Minutos del día: " + hora.toSecondOfDay() / 60)
);

Trabajar con LocalDateTime

Cuando necesitas ambas cosas, fecha y hora juntas:

// Crear fechas con hora
LocalDateTime ahoraCompleto = LocalDateTime.now();
LocalDateTime reunion = LocalDateTime.of(2025, 11, 15, 10, 30);

// Lista de eventos
List<LocalDateTime> eventos = Arrays.asList(
    LocalDateTime.of(2025, 11, 8, 9, 0),   // Hoy, reunión matutina
    LocalDateTime.of(2025, 11, 15, 10, 30), // Próxima reunión
    LocalDateTime.of(2025, 12, 25, 0, 0)    // Navidad
);

// Filtrar eventos del mes actual
eventos.stream()
    .filter(evento -> evento.getMonth() == ahoraCompleto.getMonth())
    .forEach(evento -> System.out.println("Evento este mes: " + evento));

// Ordenar eventos por fecha
eventos.stream()
    .sorted()
    .forEach(evento -> System.out.println(
        "Evento: " + evento.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))
    ));

Ejemplos Prácticos Combinados

Procesar Lista de Personas con Fechas

Un caso real: calcular edades y filtrar mayores de edad en una sola pipeline.

// Clase Persona
class Persona {
    private String nombre;
    private LocalDate fechaNacimiento;

    // constructor, getters...
}

List<Persona> personas = Arrays.asList(
    new Persona("Ana", LocalDate.of(1995, 6, 15)),
    new Persona("Carlos", LocalDate.of(1990, 12, 3)),
    new Persona("Beatriz", LocalDate.of(1998, 8, 20))
);

// Calcular edades y filtrar mayores de edad
LocalDate hoy = LocalDate.now();
personas.stream()
    .filter(p -> Period.between(p.getFechaNacimiento(), hoy).getYears() >= 18)
    .forEach(p -> {
        int edad = Period.between(p.getFechaNacimiento(), hoy).getYears();
        System.out.println(p.getNombre() + " tiene " + edad + " años");
    });

Agrupar Fechas por Mes

Collectors.groupingBy con method reference. Agrupar por mes en tres líneas:

// Agrupar fechas por mes usando lambdas
List<LocalDate> fechas = Arrays.asList(
    LocalDate.of(2025, 1, 15),
    LocalDate.of(2025, 1, 20),
    LocalDate.of(2025, 2, 10),
    LocalDate.of(2025, 2, 25),
    LocalDate.of(2025, 3, 5)
);

Map<Month, List<LocalDate>> fechasPorMes = fechas.stream()
    .collect(Collectors.groupingBy(LocalDate::getMonth));

fechasPorMes.forEach((mes, listaFechas) -> {
    System.out.println("Mes: " + mes);
    listaFechas.forEach(fecha ->
        System.out.println("  - " + fecha.format(DateTimeFormatter.ofPattern("dd/MM")))
    );
});

Mejores Prácticas

Referencias a Método (Method References)

Si tu lambda solo delega a un método existente, usa method reference. Más legible:

// En lugar de: nombre -> System.out.println(nombre)
nombres.forEach(System.out::println);

// En lugar de: fecha -> fecha.getMonth()
fechas.stream().map(LocalDate::getMonth).collect(Collectors.toList());

// En lugar de: (String s) -> s.length()
palabras.stream().mapToInt(String::length).sum();

Evitar Efectos Secundarios

Las lambdas deben ser puras cuando sea posible. Operan sobre su input y devuelven un output, sin mutar estado externo:

// ✅ Bien: Lambda pura
List<Integer> duplicados = numeros.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

// ❌ Evitar: Lambda con efectos secundarios
List<Integer> resultado = new ArrayList<>();
numeros.forEach(n -> {
    n = n * 2;
    resultado.add(n);  // Modificar estado externo
});

Manejo de Excepciones

Las lambdas no pueden lanzar excepciones verificadas directamente. Tienes dos opciones: envolver con try-catch dentro de la lambda, o extraer a un método auxiliar:

// ✅ Manejo con try-catch dentro
fechas.forEach(fecha -> {
    try {
        // Código que puede lanzar excepción
        LocalDate.parse(fecha.toString());
    } catch (DateTimeParseException e) {
        System.err.println("Fecha inválida: " + fecha);
    }
});

// ✅ Crear método auxiliar
private static void procesarFecha(LocalDate fecha) throws DateTimeParseException {
    // Código que puede lanzar excepción
}

fechas.forEach(LambdaFechasGUIA::procesarFecha);  // Envuelve la excepción

Conclusión

Las lambdas no son azúcar sintáctico. Cambian cómo piensas el código: de "cómo recorro esto" a "qué quiero obtener de esto". Empieza con forEach y filter, que son lo que vas a usar el 90% del tiempo. Cuando te sientas cómodo, mete groupingBy, partitioningBy y method references. El resto viene solo.


Basado en la documentación oficial de Oracle y el uso cotidiano. Para profundizar, los tutoriales de Oracle sobre Lambda Expressions y Stream API son un buen punto de partida.