1. El problema del enfoque imperativo
Empecemos con un ejercicio clásico. Supongamos que tenemos una lista de usuarios:
List<User> users = ...; // ya está poblada
Supongamos que necesitamos obtener la lista de nombres de todos los usuarios mayores de edad (a partir de 18 años). ¿Cómo lo hacíamos antes?
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
names.add(user.getName());
}
}
A primera vista todo es sencillo, pero fíjate en los pasos «técnicos»: declarar una nueva lista para el resultado, recorrer todos los elementos, comprobar la condición, añadir el resultado.
Si la tarea se complica —por ejemplo, necesitas obtener una lista alfabéticamente ordenada de e‑mail únicos de todos los usuarios cuyos nombres empiezan por la letra «A»—, el código crece rápidamente: filtrado, extracción de campos, eliminación de duplicados, ordenación, quizá transformaciones del formato del e‑mail, etc. Como resultado, en lugar de una lógica breve y clara obtienes una montaña de boilerplate que es difícil de leer y mantener.
Desventajas de este enfoque en dos palabras: mucho código repetitivo, alta probabilidad de cometer errores (olvidar la ordenación o tratar mal los duplicados) y dificultad para combinar operaciones. A esto se le llama estilo imperativo: describes paso a paso al ordenador cómo ejecutar la tarea, no qué quieres obtener.
2. Stream API: estilo declarativo
Con la llegada de Java 8 apareció Stream API: una herramienta que permite trabajar con colecciones en el estilo de «qué hay que hacer», y no «cómo hacerlo exactamente». A este enfoque se le llama declarativo.
¿Qué es exactamente un Stream?
No es una colección independiente, sino más bien un flujo de datos: una secuencia de elementos que pasa por una cadena de operaciones —filtrado, transformación, ordenación, recopilación del resultado, etc.—. El stream no almacena nada; simplemente «arrastra» los datos a través del pipeline. Las operaciones se pueden combinar casi como piezas de LEGO: montas la cadena y la ejecutas.
Ejemplo:
List<String> names = users.stream() // creamos un stream a partir de la lista de usuarios
.filter(u -> u.getAge() >= 18) // dejamos solo a quienes ya tienen 18 años
.map(User::getName) // convertimos User → String (tomamos el nombre)
.collect(Collectors.toList()); // recopilamos el resultado en una lista
Y ya está: en una sola línea describimos toda la tarea: «toma los usuarios, filtra por edad, extrae los nombres y recopílalos en una lista». No hace falta escribir bucles a mano, crear variables temporales ni vigilar que no se te olvide nada.
Stream API funciona como una cadena de montaje: defines las etapas de procesamiento y los datos pasan por ellas. Es bonito, compacto y mucho más legible.
3. Ventajas de Stream API
- Brevedad y legibilidad. Compara ambos enfoques —imperativo y declarativo— en la misma tarea. Se ve de inmediato cuál es más fácil de leer y mantener.
- Composición de operaciones sencilla. Es fácil «encadenar» operaciones: filter, map, sorted, collect, todo en una única secuencia coherente.
- Menos errores. Stream API te libra de la rutina: no necesitas gestionar manualmente colecciones intermedias y bucles —disminuye la probabilidad de olvidar un paso o cometer un error de índices.
- Posibilidad de paralelismo. Escalar el procesamiento a varios núcleos es fácil: basta con llamar a parallelStream() en lugar de stream(). Más detalles, más adelante.
- Estilo de programación moderno. Las ideas de la programación funcional (composición de funciones, ausencia de bucles explícitos) aumentan la expresividad y están muy valoradas en el mercado.
Breve historia de Stream API
Stream API apareció en Java 8 (2014) y supuso un salto cualitativo para el lenguaje. Antes, cualquier operación no trivial sobre colecciones requería mucho código auxiliar, mientras que otras plataformas ya ofrecían enfoques declarativos (map, filter, reduce, etc.).
Desde entonces, Stream API es el estándar de facto para el procesamiento de colecciones en Java. Si quieres escribir código Java moderno, es imprescindible.
4. Ámbitos de aplicación de Stream API
- Filtrado: seleccionar solo los elementos necesarios (por ejemplo, todos los usuarios mayores de 18 años).
- Transformación: extraer un campo o crear un nuevo objeto (por ejemplo, la lista de nombres de usuarios).
- Agregación: calcular suma, media, cantidad, máximo/mínimo.
- Ordenación: ordenar los elementos por el criterio deseado.
- Agrupación: dividir los elementos por categorías.
- Recolección del resultado: recopilar en lista, conjunto, mapa, cadena, etc.
Ejemplos de tareas:
- Obtener la lista de e‑mail de todos los usuarios cuyo nombre empieza por «A».
- Calcular la edad media de los usuarios.
- Encontrar el primer usuario con un e‑mail determinado.
- Recopilar todas las ciudades de residencia únicas en un único conjunto.
5. Detalles útiles
Cómo funciona un stream
[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/
1. filter: deja pasar solo a los usuarios que cumplen la condición.
2. map: transforma un usuario en, por ejemplo, su e‑mail.
3. collect: recopila los e‑mail en la colección deseada.
Sintaxis: cómo crear un stream
- Desde una colección: list.stream() — stream de elementos de la colección.
- Desde un array: Arrays.stream(array)
- Desde valores sueltos: Stream.of("a", "b", "c")
6. Errores típicos al pasar a Stream API
Error n.º 1: intentar modificar la colección dentro del stream. Stream API no está pensado para modificar la colección de origen (por ejemplo, no hagas list.remove() dentro de forEach). Para eliminar, utiliza, por ejemplo, removeIf en la colección antes o después del procesamiento.
Error n.º 2: confundir colección y stream. ¡Un stream no es una colección! El stream «recorre» los elementos una sola vez y después se cierra. Si necesitas procesar los datos de nuevo, crea un nuevo stream().
Error n.º 3: cadenas demasiado complejas. No conviertas la expresión en un «monstruo» de decenas de operaciones. Si la cadena se alarga, divídela en varios pasos con variables intermedias para mejorar la legibilidad y facilitar la depuración.
GO TO FULL VERSION