¿Cómo funciona la API de Streams de Java?

El lenguaje de programación Java incorpora métodos como .stream() y clases como Stream que permiten trabajar con datos mediante un sistema de pipelines. En este vídeo os hago una introducción visual que os explique cómo funciona un Stream de Java.

La API de Streams en Java nos permite procesar colecciones de datos de una manera muy visual y flexible, como si estuviéramos trabajando con una tubería por la que fluyen elementos. Esta idea no es nueva en el mundo de la programación funcional, pero su incorporación en Java facilita enormemente el manejo masivo de datos sin alterar la esencia del lenguaje.

Imaginemos que tenemos una lista con varios elementos. Al convertir esa lista en un stream, lo que hacemos es crear una tubería por la que esos elementos van pasando uno a uno cuando los vamos solicitando. Esta tubería no solo transporta datos, sino que también puede procesarlos en cada etapa. Por ejemplo, podemos transformar un elemento en otro, filtrar algunos que no cumplan ciertas condiciones o simplemente iterar sobre ellos para realizar alguna acción.

Visualmente, podemos pensar en esta tubería como una cadena de montaje. Cada elemento entra por un extremo y va pasando por diferentes estaciones donde puede ser modificado, descartado o simplemente dejado pasar. Al final, obtenemos un resultado que puede ser otra colección, un único valor o simplemente una acción realizada sobre cada elemento.

La API de Streams se organiza en tres tipos principales de funciones. Primero, están los generadores, que son los encargados de crear el stream a partir de una colección como una lista, un conjunto o un array. Por ejemplo, al llamar al método .stream() sobre una lista, obtenemos un generador que empieza a suministrar elementos uno a uno cuando se le solicita.

Luego, tenemos las operaciones intermedias, que son las piezas que procesan los datos dentro de la tubería. Un ejemplo clásico es la función filter, que recibe un predicado, es decir, una función que devuelve verdadero o falso según una condición. Si el predicado devuelve verdadero para un elemento, este continúa su camino; si no, se descarta. Otra operación intermedia es map, que transforma cada elemento en otro, permitiéndonos cambiar el tipo o el valor de los datos que fluyen.

Estas operaciones intermedias se pueden encadenar tantas veces como queramos, formando una cinta transportadora donde cada estación realiza una tarea específica. Si en algún punto un elemento no cumple una condición o es transformado, eso afecta cómo llegará al final de la tubería.

Finalmente, llegamos a los colectores o reductores, que son las funciones que consumen el stream y producen un resultado final. Por ejemplo, podemos recolectar todos los elementos que han pasado por la tubería en una nueva lista usando un colector. También podemos reducir el stream a un único valor, como la suma, la media, el máximo o el mínimo de los elementos. Esto es especialmente útil cuando queremos obtener estadísticas o resultados agregados a partir de una colección.

Para ilustrar un poco cómo funciona esto en código, imaginemos que tenemos una lista de números y queremos filtrar solo los pares, luego multiplicarlos por dos y finalmente recolectarlos en una nueva lista. El código en Java sería algo así:

List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6);

List<Integer> resultado = numeros.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * 2)
    .collect(Collectors.toList());

Aquí, numeros.stream() es el generador que crea el stream a partir de la lista. La operación intermedia filter descarta los números impares, map transforma cada número par multiplicándolo por dos, y finalmente collect es el colector que reúne los resultados en una nueva lista.

Este enfoque nos permite trabajar con colecciones de forma muy expresiva y clara, sin necesidad de escribir bucles explícitos ni manejar estados intermedios. Además, la API de Streams es perezosa, lo que significa que las operaciones intermedias no se ejecutan hasta que se invoca una operación terminal como collect, forEach o reduce. Esto optimiza el rendimiento y evita procesar elementos innecesarios.

En definitiva, la API de Streams en Java nos ofrece una forma poderosa y elegante de transformar y filtrar datos, inspirada en conceptos de programación funcional y pipelines, que facilita el trabajo con colecciones y mejora la legibilidad y mantenibilidad del código.

Lista de reproducción
  1. 1
    ¿Cómo funciona la API de Streams de Java?
    7 minutos
  2. 2
    ¿Cómo programar con .stream() en Java?
    8 minutos