Un Stream es una secuencia de elementos sobre la que encadenamos operaciones de procesamiento.
Supón que tienes una lista de usuarios y te piden: “Dame los nombres de los usuarios activos, pasados a mayúsculas y ordenados alfabéticamente”.
El Enfoque Antiguo (Imperativo - Java < 8): Tienes que decirle al ordenador CÓMO hacerlo paso a paso.
List<String> resultado = new ArrayList<>();
for (Usuario u : usuarios) {
if (u.isActivo()) { // 1. Filtrar
String nombre = u.getNombre();
resultado.add(nombre.toUpperCase()); // 2. Transformar
}
}
Collections.sort(resultado); // 3. Ordenar
for (String s : resultado) {
System.out.println(s); // 4. Imprimir
}
Esto es feo. Mezclamos lógica de negocio (“es activo”) con lógica de iteración (for, if).
El Enfoque Moderno (Declarativo - Java 8+): Solo dices QUÉ quieres conseguir.
usuarios.stream()
.filter(u -> u.isActivo()) // 1. Filtrar
.map(u -> u.getNombre().toUpperCase()) // 2. Transformar
.sorted() // 3. Ordenar
.forEach(System.out::println); // 4. Imprimir
Bienvenidos a la Stream API.
¿Qué es un stream?
Un Stream (Flujo) no es una estructura de datos. No guarda datos.
Es una cadena de montaje (pipeline) por la que pasan los datos para ser procesados.
Piensa en una cinta transportadora de una fábrica:
- Fuente: Salen las piezas (la Lista original).
- Operaciones Intermedias: Robots que manipulan las piezas (Filtran, Pintan, Cortan).
- Operación Terminal: El empaquetado final.
Inmutabilidad: Las operaciones de Stream no modifican por sí mismas la colección origen. Aun así, una lambda con efectos laterales podría cambiar objetos o estado externo, por lo que conviene evitarla.
Las 3 fases de un stream
Todo Stream tiene siempre esta estructura:
La fuente (.stream())
Convertimos una Colección (List, Set) en un flujo.
usuarios.stream()…
Operaciones intermedias (transformadores)
Son métodos que devuelven otro Stream. Se pueden encadenar infinitamente. Aquí es donde usamos las Interfaces Funcionales que aprendimos en el artículo anterior.
-
.filter(Predicate): Deja pasar solo los elementos que cumplen la condición. -
Ejemplo:
.filter(u -> u.getEdad() > 18) -
.map(Function): Transforma cada elemento en otra cosa. -
Ejemplo: Entra
Usuario-> SaleString(su nombre). -
Analogía: Entra Naranja -> Sale Zumo.
-
.sorted(): Ordena el flujo (usando Comparable o un Comparator). -
.distinct(): Elimina duplicados (usando equals/hashCode).
Operación terminal (el gatillo)
Son métodos que cierran el Stream y producen un resultado (un valor, una lista nueva o un efecto secundario). Sin operación terminal, el Stream NO ARRANCA.
.forEach(Consumer): Ejecuta una acción para cada elemento (ej: imprimir)..count(): Cuenta cuántos elementos han sobrevivido..collect(...): Guarda el resultado en una nueva Lista (lo veremos en el próximo post).
Ejemplo completo: análisis de ventas
Supongamos una lista de Venta (producto, importe). Queremos los importes de las ventas mayores a 100€, añadiéndoles el IVA.
List<Venta> ventas = repositorio.getVentas();
ventas.stream() // 1. Abrimos el grifo
.filter(v -> v.getImporte() > 100) // Predicate: Solo las caras
.map(v -> v.getImporte() * 1.21) // Function: Venta -> Double (con IVA)
.forEach(precio -> System.out.println("Precio final: " + precio)); // Consumer
Concepto clave: laziness
Los Streams son perezosos (Lazy Evaluation). Cuando escribes .filter(...) o .map(...), Java no está ejecutando nada todavía. Solo está “tomando nota” de las instrucciones.
Solo cuando llamas a la Operación Terminal (como .forEach o .count), el agua empieza a fluir y se ejecutan todas las operaciones de golpe.
Esto permite optimizaciones brutales: si solo pides el primer elemento (.findFirst()), el Stream dejará de procesar el resto de la lista en cuanto encuentre uno, aunque la lista tenga 1 millón de datos.
Un error común: reutilizar un stream
Un Stream es de un solo uso. Una vez que el agua ha pasado y ha llegado a la operación terminal, el Stream se cierra.
Stream<String> miStream = lista.stream().filter(s -> s.length() > 3);
miStream.forEach(System.out::println); // ✅ Funciona
miStream.count(); // ❌ EXCEPCIÓN: Stream has already been operated upon or closed
Si necesitas volver a procesar los datos, tienes que crear un nuevo Stream desde la fuente (lista.stream()).