W3docs

Introducción a los Streams de Java

Introducción al Stream API de Java para procesar secuencias de elementos con operaciones de estilo funcional.

Un stream es un pipeline que lleva los elementos de una fuente a través de una secuencia de operaciones y produce un resultado. No es una estructura de datos — no almacena nada. Es una receta declarativa para procesar datos, evaluada de forma perezosa, ejecutada una sola vez. Los streams llegaron en Java 8 junto con las lambdas, y ambos fueron diseñados para encajar: cada operación de stream acepta una función, y el lenguaje te dio una forma clara de escribirla.

La forma que escribirás cientos de veces:

double avgAdultAge = people.stream()
    .filter(p -> p.age() >= 18)
    .mapToInt(Person::age)
    .average()
    .orElse(0.0);

Tres cosas a notar. El pipeline se lee de arriba a abajo como pasos que describen qué quieres, no cómo iterar. Cada paso toma una función — un Predicate, un ToIntFunction — exactamente el vocabulario que los capítulos anteriores establecieron. Y el resultado sale de una única operación terminal; no hay bucle, no hay acumulador, no hay continue anticipado.

La forma del pipeline: fuente → intermedia → terminal

Todo pipeline de stream tiene tres partes:

  1. Una fuente. De dónde provienen los elementos. Generalmente una colección (coll.stream()), ocasionalmente un literal (Stream.of(\"a\", \"b\")), un array (Arrays.stream(arr)), un rango IntStream (IntStream.range(0, 100)), una fuente de I/O (Files.lines(path)), o un generador (Stream.iterate, Stream.generate). El siguiente capítulo está dedicado a todos ellos.
  2. Cero o más operaciones intermedias. Cada una devuelve otro stream, por lo que se encadenan. Las más comunes: filter, map, flatMap, distinct, sorted, limit, skip, peek. Son perezosas — llamar a filter no evalúa nada todavía; simplemente registra el predicado.
  3. Exactamente una operación terminal. Activa el pipeline. Ejemplos: forEach, collect, toList, count, sum, min, max, reduce, findFirst, anyMatch. La terminal produce un valor (o un efecto secundario en el caso de forEach) y consume el stream — no puedes reutilizarlo.
list.stream()              // SOURCE
    .filter(...)           // intermediate
    .map(...)              // intermediate
    .sorted()              // intermediate
    .toList();             // TERMINAL — runs the pipeline

Sin la terminal, no ocurre nada. Un stream que construyes y nunca terminas es peso muerto — no se realiza ningún trabajo, no se disparan efectos secundarios, las lambdas no se ejecutan.

Perezoso por diseño

Las operaciones intermedias son perezosas porque la JVM no sabe qué elementos necesitas realmente hasta que la terminal los solicita. Eso permite dos optimizaciones importantes:

Fusión. Los intermedios adyacentes se ejecutan juntos en un solo paso, no un paso por operación. stream.filter(p).map(f) no construye una lista filtrada intermedia y luego la mapea; evalúa un elemento, y si sobrevive, lo mapea, todo en un solo paso.

Cortocircuito. Una terminal como findFirst, anyMatch, o limit(n) detiene el pipeline tan pronto como tiene su respuesta. Combinado con la pereza, esto significa que puedes ejecutar un pipeline de "encuentra el primer cuadrado par mayor que 100" sobre un stream infinito y obtener una respuesta en microsegundos:

int answer = Stream.iterate(1, n -> n + 1)         // 1, 2, 3, 4, ...
    .map(n -> n * n)                                // 1, 4, 9, 16, ...
    .filter(n -> n % 2 == 0 && n > 100)             // first match wins
    .findFirst()
    .orElseThrow();
// answer = 144

Stream.iterate(1, n -> n + 1) es infinito, pero findFirst solo solicitó elementos hasta que uno coincidió. El pipeline evaluó 12 cuadrados (1, 4, 9, ..., 144) y se detuvo.

De un solo uso, como un Iterator

Un Stream puede recorrerse una sola vez. La terminal lo consume, y después de eso el objeto stream está cerrado; llamar a otra terminal sobre él lanza IllegalStateException:

Stream<String> s = list.stream();
long c1 = s.count();             // ok
long c2 = s.count();             // throws IllegalStateException — stream has already been operated upon

Si necesitas procesar los mismos datos dos veces, construye el stream dos veces:

long c1 = list.stream().count();
long c2 = list.stream().count();

Esto coincide con la forma en que funciona Iterator. El objeto stream es el cursor en movimiento, no los datos. Los datos son la fuente — volver a crear el stream es gratuito.

Streams vs colecciones — trabajos diferentes

AspectoColecciónStream
¿Almacena datos?No
¿Reutilizable?No (una sola terminal)
¿Ansioso o perezoso?AnsiosoPerezoso hasta la terminal
¿Modifica la fuente?Sí (ej. list.add)No — los pipelines son de solo lectura
¿Itera explícitamente?A menudo (for, iterator())No — el pipeline conduce la iteración
Modelo de costeGestión por elementoUn solo paso a través de la fuente

Una colección es un contenedor; un stream es una computación sobre un contenedor (u otra fuente). Se complementan: obtienes datos de una colección, ejecutas un pipeline de stream y recopilas el resultado en una colección (generalmente diferente).

Tres pequeños ejemplos que escribirás todo el tiempo

Contar elementos que coincidan con un predicado:

long adults = people.stream().filter(p -> p.age() >= 18).count();

Construir una lista de valores transformados:

List<String> names = people.stream().map(Person::name).toList();

Reducir a un único valor:

int totalAge = people.stream().mapToInt(Person::age).sum();

Estos tres patrones — count, map-to-list, reduce-to-scalar — cubren la mayoría de los usos del API. El resto de la parte es un recorrido por las operaciones que completan el cómo para cada uno.

Tres cosas que los streams no son

  • No son un reemplazo para los bucles for en general. Un bucle que construye algo con flujo de control no trivial, que necesita break con efectos secundarios, o que muta varias variables, sigue siendo más claro como bucle. Los streams brillan cuando el trabajo es un pipeline de operaciones puras.
  • No son una mejora de rendimiento en datos pequeños. Un pipeline de stream asigna algunos objetos pequeños; un bucle de 10 elementos lo superará. Las ganancias vienen de la claridad con cualquier dato y del paralelismo con datos grandes.
  • No son un sustituto de Iterator/Iterable cuando otro código los espera. Un stream produce valores; si necesitas intercalar el consumo (un for mejorado, una List devuelta desde un método), usa toList() primero.

Secuencial por defecto, paralelo bajo petición

Cada stream que escribirás en este capítulo es secuencial — los elementos fluyen a través del pipeline de uno en uno, en orden. También existe coll.parallelStream() (y stream.parallel()) que programa el pipeline a través del ForkJoinPool común para trabajo multinúcleo. Los streams paralelos son un capítulo posterior — hacen varias suposiciones sobre el pipeline (debe ser asociativo, sin estado, sin efectos secundarios) que los pipelines de "introducción" de este capítulo cumplen de forma natural, por lo que la actualización suele ser un cambio de un solo token.

Un ejemplo trabajado: un pipeline completo, pereza y la regla de uso único

El programa de abajo construye una pequeña lista de registros Person, ejecuta la forma canónica del pipeline (filter → map → sorted → collect), demuestra la pereza con peek, muestra el cortocircuito en un Stream.iterate infinito, y muestra la IllegalStateException que obtienes al reutilizar un stream.

java— editable, runs on the server

Lo que hay que sacar de la ejecución:

  • El pipeline canónico de cuatro pasos — streamfiltermaptoList — produjo una lista ordenada de nombres de adultos sin ningún bucle explícito, sin colección temporal y sin gestión de nulabilidad.
  • peek imprimió una vez por cada elemento extraído. findFirst extrajo elementos hasta que uno satisfizo n*n > 50 (lo que ocurre en n = 8, cuadrado 64) y luego se detuvo. Eso es la pereza y el cortocircuito trabajando juntos: las operaciones anteriores hicieron exactamente el trabajo necesario y nada más.
  • El pipeline "primer cuadrado par mayor que 100" se ejecutó sobre una fuente infinita. Sin cortocircuito eso sería un bucle infinito; con él el pipeline evaluó 12 valores y produjo 144.
  • El segundo s.count() lanzó IllegalStateException. Los streams son de uso único; si necesitas un segundo paso, construye un stream fresco desde la fuente.
  • El pipeline "sin terminal" al final no imprimió nada desde dentro de su peek. Sin una terminal, los intermedios no se ejecutan — el stream es simplemente una receta que nadie pidió ejecutar.

Qué viene a continuación

Ya conoces la forma del pipeline, la división fuente/intermedia/terminal, el contrato de pereza y la regla de uso único. El siguiente capítulo, Creación de Streams de Java, es el catálogo de fuentesCollection.stream(), Stream.of, Arrays.stream, IntStream.range, Stream.iterate, Stream.generate, Files.lines, String.chars(), Stream.empty y el API Stream.Builder. Con el capítulo de fuentes completado tendrás todo lo que necesitas para empezar, y el resto de la parte completará las operaciones intermedias y terminales.

Práctica

Práctica
Escribes `list.stream().filter(p).map(f);` y no llamas a ninguna operación terminal. ¿Qué ocurre cuando se ejecuta esta línea?
Escribes `list.stream().filter(p).map(f);` y no llamas a ninguna operación terminal. ¿Qué ocurre cuando se ejecuta esta línea?
Was this page helpful?