
En Java, las lambdas y streams son características introducidas en Java 8 que permiten un estilo de programación funcional.
Las lambdas se utilizan para crear funciones de forma concisa, permiten escribir código de forma más clara y compacta al definir rápidamente métodos en línea sin la necesidad de crear clases anónimas.
Los streams facilitan el procesamiento de colecciones de datos de forma declarativa y paralela.
Juntos, proporcionan una forma moderna y poderosa de manipular datos en Java.
Las expresiones lambda son funciones sin nombre que permiten pasar comportamientos como argumentos, simplificando significativamente la implementación de interfaces funcionales.
Reducen drásticamente la cantidad de código necesario para implementar interfaces con un único método, eliminando la necesidad de crear clases anónimas extensas.
Constituyen el fundamento de la programación funcional en Java, un paradigma que se centra en el uso de funciones puras y la inmutabilidad de los datos.
(parámetros) -> { cuerpo de la función }
Para un único parámetro, se pueden omitir los paréntesis
Para una única instrucción, se pueden omitir las llaves y el return
Para múltiples instrucciones, se requieren llaves y return explícito
- Ejemplo 1: (x) -> x * 2
- Ejemplo 2: (nombre) -> { return "Hola, " + nombre; }
- Ejemplo 3: (numeros) -> { int suma = 0;
for (int num : numeros) {
suma += num; }
return suma; }
- Ejemplo 4: () -> System.out.println("Hola, mundo!")
- Ejemplo 5: (a, b, c) -> a * b + c
- Ejemplo 6: (texto) -> "¡Hola " + texto + "!"
- Ejemplo 7: (array) -> { int max = array[0];
for (int num : array) {
if (num > max) max = num;}
return max; La referencia a métodos (Method References) es una característica introducida en Java 8 que proporciona una sintaxis abreviada para expresar lambdas que simplemente invocan un método existente.
Es una forma de expresión lambda más compacta y legible cuando el lambda solo llama a un método existente.
Referencia a método estático (Class::staticMethod)
Function<String, Integer> parser = Integer::parseInt;Referencia a método de instancia de un objeto particular (object::instanceMethod)
String str = "ejemplo";
Supplier<Integer> lengthSupplier = str::length;Referencia a método de instancia de un tipo arbitrario (Class::instanceMethod)
Function<String, String> upperCase = String::toUpperCase;Referencia a constructor (Class::new)
Supplier<List<String>> listSupplier = ArrayList::new;La sintaxis general sigue el patrón:
Copy
ClassOrInstance::methodNameDonde:
:: es el operador de referencia a métodoLas referencias a métodos son válidas cuando:
Una interfaz funcional es una interfaz que contiene exactamente un método abstracto (puede tener métodos default o estáticos adicionales). Se usan principalmente para expresar lambdas y referencias a métodos.
Las interfaces funcionales son una característica importante introducida en Java 8 que permite la programación funcional en Java.
@FunctionalInterface (opcional pero recomendado)java.util.functionJava 8 incluye varias interfaces funcionales comunes en el paquete java.util.function:
Transforma un objeto de tipo T en otro de tipo R.
Evalúa una condición sobre un objeto de tipo T y devuelve un valor booleano.
Acepta un objeto de tipo T y realiza una operación sin devolver ningún resultado.
No recibe parámetros pero produce un resultado de tipo T.
Tipo especial de Function donde el argumento y el resultado son del mismo tipo
Acepta dos argumentos del mismo tipo y devuelve un resultado del mismo tipo
@FunctionalInterface
interface Calculadora {
int operacion(int a, int b);
}
public class Test {
public static void main(String[] args) {
Calculadora suma = (x, y) -> x + y;
Calculadora resta = (x, y) -> x - y;
System.out.println(suma.operacion(5, 3)); // 8
System.out.println(resta.operacion(5, 3)); // 2
}
}Creación del stream a partir de una colección, array u otra fuente de datos.
Transformaciones que devuelven un nuevo stream (filter, map, sorted).
Produce un resultado final o efecto secundario (collect, forEach, reduce).
Los streams representan un pipeline de procesamiento de datos que permite operar sobre colecciones de manera declarativa.
A diferencia del enfoque imperativo tradicional, donde especificamos cómo realizar una tarea, con streams expresamos qué queremos lograr, delegando los detalles de implementación al framework.
Características fundamentales de los streams:
Toda implementación de Collection (List, Set, Queue, etc.) puede generar un Stream:
// Desde List
List lista = Arrays.asList("a", "b", "c");
Stream streamDesdeLista = lista.stream();
// Desde Set
Set numeros = Set.of(1, 2, 3);
Stream streamDesdeSet = numeros.stream();
// Desde cualquier Collection
Collection valores = new ArrayList<>(Arrays.asList(1.1, 2.2, 3.3));
Stream streamDesdeCollection = valores.stream();
La clase Arrays proporciona métodos para crear streams directamente desde arrays.
// Array de objetos
String[] arrayStrings = {"a", "b", "c"};
Stream streamDesdeArray = Arrays.stream(arrayStrings);
// Array primitivo (devuelve un IntStream, LongStream o DoubleStream especializado)
int[] numerosPrimitivos = {1, 2, 3};
IntStream streamDesdePrimitivos = Arrays.stream(numerosPrimitivos);
La interfaz Stream ofrece métodos factory para crear streams de diferentes maneras.
Stream.of() : Crea un Stream con unos elementos concretos o incluso vacío
Stream streamDirecto = Stream.of("a", "b", "c"); // Stream de elementos concretos
Stream<?> streamVacio = Stream.empty(); // Stream vacío
Stream.iterate(): Para secuencias
// Stream infinito (limitado con limit())
Stream numerosPares = Stream.iterate(0, n -> n + 2) .limit(10); // 0, 2, 4, 6, ..., 18
// Versión moderna (Java 9+) con condición de parada
Stream numerosMenores100 = Stream.iterate(0, n -> n < 100, n -> n + 5);
Stream.generate(): Para valores generados
// Stream infinito de números aleatorios
Stream aleatorios = Stream.generate(Math::random) .limit(5);
// Stream de elementos constantes
Stream holas = Stream.generate(() -> "Hola") .limit(3); // "Hola", "Hola", "Hola"
Las operaciones intermedias son perezosas: no realizan ningún procesamiento hasta que se invoca una operación terminal.
Esto permite la optimización interna del pipeline, como la fusión de operaciones o la omisión de procesamiento innecesario.
Selecciona elementos que cumplen un predicado:
stream.filter(n -> n > 0)
Transforma cada elemento:
stream.map(s -> s.toUpperCase())
Ordena los elementos:
stream.sorted() o stream.sorted(Comparator.reverseOrder())
Elimina elementos duplicados:
stream.distinct()
Limita o salta elementos:
stream.limit(10).skip(2)
Una característica clave es que cada operación intermedia devuelve un nuevo stream, lo que permite el encadenamiento de múltiples operaciones en una única expresión fluida y legible.
Las operaciones terminales son las que finalmente producen un resultado concreto a partir del stream. Inician el procesamiento de todos los elementos a través del pipeline y finalizan el ciclo de vida del stream. Después de ejecutar una operación terminal, el stream se considera consumido y no puede reutilizarse.
Acumula elementos en una colección
Agregación de elementos
Verificación de condiciones
Iteración sobre elementos
Operaciones simples
El método collect() es particularmente potente, ya que permite recopilar los elementos del stream en estructuras de datos como listas, conjuntos o mapas, aplicando diferentes estrategias de acumulación definidas por la clase Collectors.
El uso de Predicate permite modularizar condiciones de filtrado para su reutilización o combinación mediante métodos como and(), or() y negate():
Predicate esCaro = p -> p.getPrecio() > 100;
Predicate esElectronica = p -> p.getCategoria().equals("Electrónica");
Predicate filtroCompuesto = esCaro.and(esElectronica);
List resultado = productos.stream()
.filter(filtroCompuesto)
.collect(Collectors.toList());
Convertir cada elemento aplicando una función sencilla
Extraer o calcular propiedades de objetos complejos
Convertir objetos de un tipo a otro (mapeo de entidades)
Procesar colecciones dentro de objetos
La operación map() es una de las herramientas más versátiles en streams, ya que permite transformar cada elemento de la secuencia aplicando una función.
La función recibe un elemento como entrada y produce un elemento de salida, que puede ser del mismo tipo o de un tipo diferente.
El método reduce() combina todos los elementos de un stream en un único resultado mediante una operación binaria.
// Sumar todos los números
int suma = numeros.stream().reduce(0, (a, b) -> a + b);
// O usando método de referencia
int suma = numeros.stream().reduce(0, Integer::sum);
El valor de identidad es el elemento neutro de la operación (0 para suma, 1 para multiplicación).
// Multiplicar todos los números
int producto = numeros.stream().reduce(1, (a, b) -> a * b);
Para objetos más complejos, se puede utilizar un acumulador y un combinador.
// Encontrar el precio total
BigDecimal total = productos.stream().map(Producto::getPrecio) .reduce(BigDecimal.ZERO, BigDecimal::add);
La reducción es una operación fundamental en la programación funcional que permite combinar múltiples valores en uno solo. En Java, el método reduce() permite implementar operaciones como suma, multiplicación, concatenación o cualquier otra función de agregación personalizada.
La clase Collectors proporciona implementaciones de la interfaz Collector que ofrecen operaciones sofisticadas para acumular elementos en estructuras de datos complejas, realizar agrupaciones, particiones y cálculos estadísticos.
Estos colectores se utilizan principalmente con el método collect() de Stream.
Recopila elementos en la estructura de datos especificada:
List lista = stream.collect(Collectors.toList());
Map mapa = personas.stream().collect(Collectors.toMap(Persona::getId, Persona::getNombre));
Agrupa elementos según una función clasificadora:
Map porDepartamento = empleados.stream().collect(Collectors.groupingBy(
Empleado::getDepartamento));
Particiona elementos según un predicado:
Map porSalario =empleados.stream().collect(Collectors.partitioningBy(
e -> e.getSalario() > 30000));
Generan estadísticas descriptivas:
DoubleSummaryStatistics stats =
productos.stream()
.collect(Collectors.summarizingDouble(Producto::getPrecio));
// stats.getAverage(), getCount(),
// getMax(), getMin(), getSum()
Optional es un contenedor que puede contener o no un valor no nulo.
Fue introducido en Java 8 para representar explícitamente la posibilidad de ausencia de valor, evitando así las excepciones NullPointerException y haciendo el código más claro respecto al manejo de valores potencialmente nulos.
Optional resultado = personas.stream()
.filter(p -> p.getEdad() > 30)
.map(Persona::getEmail)
.filter(email -> email.contains("@"))
.findFirst();
String email = resultado.orElse("Sin email válido");
La clase Optional se integra perfectamente con streams, especialmente en operaciones que pueden no producir resultados, como findFirst(), findAny(), min() o max(). El uso correcto de Optional mejora la robustez del código y comunica claramente la intención del programador respecto al manejo de valores opcionales.
La adopción de un estilo funcional con lambdas y streams requiere un cambio de mentalidad.
Es importante recordar que el objetivo final es mejorar la legibilidad, mantenibilidad y expresividad del código, no solo reducir su longitud o maximizar su rendimiento a toda costa.
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.Period;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class EjemploCompleto {
public static void main(String[] args) {
// Lista de empleados para procesar
List empleados = Arrays.asList(
new Empleado("Ana", "Desarrollo", LocalDate.of(1985, 5, 21), new BigDecimal("45000")),
new Empleado("Carlos", "Marketing", LocalDate.of(1992, 8, 10), new BigDecimal("38000")),
new Empleado("Elena", "Desarrollo", LocalDate.of(1989, 3, 15), new BigDecimal("47000")),
new Empleado("David", "Ventas", LocalDate.of(1991, 7, 8), new BigDecimal("52000")),
new Empleado("Sofía", "Desarrollo", LocalDate.of(1995, 11, 23), new BigDecimal("41000")),
new Empleado("Miguel", "Marketing", LocalDate.of(1988, 2, 14), new BigDecimal("39500"))
);
// Calcular la edad promedio por departamento
Map edadPromedioPorDepartamento = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getDepartamento,
Collectors.averagingInt(e ->
Period.between(e.getFechaNacimiento(), LocalDate.now()).getYears())
));
System.out.println("Edad promedio por departamento:");
edadPromedioPorDepartamento.forEach((depto, edad) ->
System.out.printf("%s: %.1f años\n", depto, edad));
// Encontrar el empleado mejor pagado por departamento
Map mejorPagadoPorDepartamento = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getDepartamento,
Collectors.collectingAndThen(
Collectors.maxBy((e1, e2) ->
e1.getSalario().compareTo(e2.getSalario())),
opcional -> opcional.orElse(null)
)
));
System.out.println("\nEmpleado mejor pagado por departamento:");
mejorPagadoPorDepartamento.forEach((depto, emp) ->
System.out.printf("%s: %s (%s)\n", depto, emp.getNombre(), emp.getSalario()));
// Calcular estadísticas salariales
System.out.println("\nEstadísticas salariales:");
empleados.stream()
.map(Empleado::getSalario)
.mapToDouble(BigDecimal::doubleValue)
.summaryStatistics()
.forEach(stats -> {
System.out.printf("Mínimo: %.2f\n", stats.getMin());
System.out.printf("Máximo: %.2f\n", stats.getMax());
System.out.printf("Promedio: %.2f\n", stats.getAverage());
System.out.printf("Total: %.2f\n", stats.getSum());
});
}
}
class Empleado {
private final String nombre;
private final String departamento;
private final LocalDate fechaNacimiento;
private final BigDecimal salario;
// Constructor, getters y setters...
}




Es común enfrentar NullPointerException al trabajar con colecciones que contienen elementos nulos o al acceder a propiedades de objetos que podrían ser nulos.
Solución: Utilizar filter para eliminar elementos nulos antes de procesarlos, o mapear con Optional para gestionar valores potencialmente nulos.
stream.filter(Objects::nonNull)
.map(obj -> Optional.ofNullable(obj.getPropiedad()))
.filter(Optional::isPresent)
.map(Optional::get)
Error "stream has already been operated upon or closed" al intentar utilizar un stream después de una operación terminal.
Solución: Los streams no son reutilizables. Se debe crear un nuevo stream cada vez que se necesite procesar la colección.
// Incorrecto:
Stream stream = lista.stream();
stream.forEach(System.out::println);
long count = stream.count(); // Error!
// Correcto:
lista.stream().forEach(System.out::println);
long count = lista.stream().count();
Integración completa con lambdas y streams para operaciones de datos
Uso de streams para procesar resultados de consultas
Project Reactor y RxJava aprovechan conceptos similares a streams
Procesamiento eficiente de datos en APIs
El ecosistema Java moderno ha adoptado ampliamente el paradigma funcional introducido con lambdas y streams. Frameworks populares como Spring han integrado estas características en sus APIs, facilitando operaciones como:
La programación reactiva, una tendencia creciente en el desarrollo Java, utiliza conceptos similares a los streams para manejar flujos de datos asíncronos con operadores como map, filter y reduce. Bibliotecas como Project Reactor (usado en Spring WebFlux) y RxJava permiten crear aplicaciones altamente escalables utilizando patrones funcionales.
Lambdas y Streams en Java