Menu

Streams de Java: filter, map, collect y reduce explicados

Cómo procesar colecciones con la Stream API de Java - filter, map, sorted, collect, count y reduce - construyendo pipelines legibles en lugar de bucles manuales.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Pipelines en lugar de bucles

Un stream es un pipeline para procesar una secuencia de valores. Partes de una fuente - normalmente una colección - encadenas operaciones como "conserva solo estos", "transforma cada uno", "ordénalos" y terminas recogiendo el resultado. En lugar de escribir un bucle con una lista temporal y un if dentro, describes lo que quieres y dejas que el stream haga el recorrido.

Ahora que tienes lambdas, los streams encajan a la perfección: cada operación recibe una lambda (o una referencia a método) que describe el trabajo para un solo elemento.

Léelo de arriba a abajo: toma los nombres, conserva los largos, pásalos a mayúsculas, recógelos en una lista. La lista original names nunca se modifica - un stream produce un resultado nuevo y deja la fuente intacta.

La anatomía de un stream

Todo pipeline tiene tres partes:

  • Una fuente - list.stream(), Arrays.stream(array) o Stream.of(a, b, c).
  • Cero o más operaciones intermedias - filter, map, sorted, distinct, limit. Cada una devuelve otro stream, por lo que puedes encadenarlas.
  • Exactamente una operación terminal - collect, count, forEach, reduce, findFirst. Es la que pone en marcha el trabajo y produce un resultado (o un efecto secundario).

Esta división importa por una regla que sorprende a todo el mundo al principio.

Las operaciones intermedias son perezosas

Las operaciones intermedias no hacen nada por sí solas. Solo registran lo que debería ocurrir. El pipeline únicamente se ejecuta cuando una operación terminal pide un resultado.

Ejecútalo: la lambda de map nunca se ejecuta. Añade una operación terminal y toda la cadena cobra vida:

Esta pereza no es un capricho contra el que luchar - permite al stream fusionar operaciones y omitir el trabajo que no necesita. Pero significa que un pipeline sin operación terminal no hace nada, lo cual es un error común del tipo "¿por qué no pasa nada?".

filter y map: conservar y transformar

Estas dos cargan con casi todo el peso. filter recibe un Predicate (una lambda que devuelve boolean) y conserva los elementos que lo superan. map recibe una Function y reemplaza cada elemento por el resultado de aplicarla.

Observa que map puede cambiar el tipo del elemento: aquí un stream de String se convierte en un stream de Integer. El String::length es una referencia a método - una forma abreviada de la lambda w -> w.length().

Las operaciones terminales producen un resultado

Una vez que has dado forma al stream, una operación terminal lo convierte en algo concreto:

Operaciones terminales habituales: collect (reunir en una lista/conjunto/mapa), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max y reduce. Después de ejecutar una terminal, el stream queda consumido - no puedes reutilizarlo. Llama de nuevo a list.stream() para obtener un pipeline nuevo.

Recoger los resultados

collect junto con Collectors es el caballo de batalla para construir un resultado. El más habitual es Collectors.toList():

Collectors también te ofrece toSet(), joining(...), groupingBy(...) y counting(). En Java 16 y posteriores puedes reemplazar collect(Collectors.toList()) por el más corto .toList() (devuelve una lista no modificable):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted, distinct y limit

Estas operaciones intermedias remodelan el stream antes de que lo recojas:

sorted() sin argumentos usa el orden natural; pasa un Comparator (aquí una lambda) para un orden personalizado. Comparator.reverseOrder() es la forma más limpia de ordenar de forma descendente.

reduce: plegar un stream a un único valor

Cuando necesitas combinar todos los elementos en un único resultado - una suma, un producto, la cadena más larga - reduce es la herramienta general. Le das un valor inicial y una función que fusiona dos valores:

Para sumas y promedios simples, los streams especializados son más claros: nums.stream().mapToInt(Integer::intValue).sum(). Recurre a reduce cuando no haya un agregador ya listo.

Los streams no reemplazan a todos los bucles

Los streams brillan cuando estás transformando una colección en un resultado. No son automáticamente más rápidos que un bucle, y resultan incómodos cuando necesitas mutar estado externo o salir anticipadamente de formas enrevesadas. Una buena regla: si el pipeline se lee como una frase clara, usa un stream; si estás echando mano de un contador compartido o de un index, un simple bucle for es honesto y está bien.

Recuerda también que un stream es de un solo uso. Esto lanza una excepción:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

Siguiente: Optional

Varias operaciones terminales de los streams - findFirst, min, max, reduce sin identidad - podrían no encontrar nada, así que no devuelven un valor desnudo. Devuelven un Optional, el contenedor de Java para "quizá un valor, quizá nada", que por fin te da una alternativa limpia a devolver null. Esa es la siguiente página.

Preguntas frecuentes

¿Qué es un stream en Java?

Un stream es un pipeline para procesar una secuencia de elementos - normalmente provenientes de una colección - a través de una cadena de operaciones como filter, map y sorted, que termina en una operación terminal como collect o count. No almacena datos y no modifica la fuente; describe un cálculo. Construyes uno con list.stream() y lees la cadena de arriba a abajo, como si fuera una receta.

¿Cómo se convierte un stream de nuevo en una lista en Java?

Termina el pipeline con un colector terminal: list.stream().filter(...).collect(Collectors.toList()). En Java 16+ puedes usar el más corto .toList(), que devuelve una lista no modificable. Sin una operación terminal no se ejecuta nada en absoluto, porque las operaciones intermedias como filter y map son perezosas.

¿Cuándo deberías usar un stream en lugar de un bucle for?

Opta por un stream cuando estás transformando o filtrando una colección para obtener un resultado - el pipeline se lee como una frase clara ("toma los nombres, conserva los largos, pásalos a mayúsculas, recógelos en una lista"). Quédate con un simple bucle for cuando necesites mutar estado externo, salir anticipadamente de formas complejas, o cuando la lógica sea más sencilla como pasos imperativos. Los streams buscan la claridad, no la velocidad pura.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR