Expresiones Lambda en Java: Guía Esencial para Recorridos y Fechas
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ónConclusió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.