
Java Streams
En un artículo anterior hablamos de Lambdas y de cómo nos permiten pasar código como parámetro. En el presente hablaremos de Java Streams, que sirven para simplificar el uso de las colecciones. Nos permitirán filtrar y recorrer los datos de manera más rápida y eficiente.

Yo mismo después de leer el párrafo anterior
Mejor veamos un ejemplo:
¿Cómo recorremos una colección de forma «clásica»?
Seguro que te suena este código
El fragmento resaltado en rojo se utiliza para «recorrer» la lista de nombres y mostrarla en pantalla. Nada enrevesado. Ahora veamos como sería usando Streams
¿Cómo recorremos una colección con Java Streams?
Como puede apreciarse, no es tanto que sea más corto (que lo es) sino que es mas «descriptivo». Casi se puede leer como lenguaje natural:
«Con la lista de nombres, creas un stream y para cada elemento de ese stream lo muestras con println».
Fíjate que estamos usando un «lambda», aquí:
Es decir, que la función forEach «recibe código» directamente, no una variable! El truco es que un Lambda realmente se recoge en un objeto, pero eso es transparente para nosotros. Como programadores, estamos pasando un comportamiento como parámetro en lugar de un dato.
¿Todas las funciones pueden recibir un lambda como parámetro?
Respuesta corta
No. Sólo aquellas funciones «diseñadas» para recibirlos.
Respuesta larga
Los Streams son una librería nueva en Java, diseñada específicamente para potenciar su uso con lambdas. Por eso verás que muchas de las funciones de streams (como forEach que ya hemos visto, o map y filter que veremos más adelante) reciben un tipo de objeto específico. Vamos, que no puedes pasar un «Integer» y quedarte tan ancho.
Si tienes curiosidad por las interfaces funcionales (que son el tipo de dato que representa a un Lambda) puedes verlas todas aquí:
Documentación oficial de Oracle: Interfaces funcionales
Debo conocerlas todas para usarlas?
No, con 3 o 4 de ellas ya puedes sacar partido de los Streams. A saber:
En el caso de Stream.forEach, recibe un objeto de tipo Consumer. Aquí está la firma de la función:
void significa que «forEach» no va a retornar nada: Solo hace «algo» que le pedimos, en este caso System.out.println.
En cambio sí recibe un parámetro, y casualmente de tipo Consumer. Esta interface contiene el método accept:
Consumer tampoco retorna nada (void) pero sí recibe un parámetro, de cualquier tipo (por eso la T).
Pasando un objeto Consumer a Stream.forEach
Este método accept es el que forEach utilizará para ejecutar el lambda que le pasemos.
Para aclarar esto, una imagen vale más que mil palabras:
Este fragmento de código es equivalente al de la sección ¿Cómo recorremos una colección con un Stream?
Como puede verse, podemos crear un objeto de tipo Consumer que se encargará del system.out y pasar esa variable (mi_consumer) como argumento al forEach. Esta forma es un poco más tradicional, pero a fin de cuentas funciona igual:
- Creamos un lambda con la acción que se va a realizar
- Creamos un stream
- Invocamos a forEach para que se aplique el lambda (miConsumer) a CADA ELEMENTO del stream.
Si pudiéramos ver cómo funciona forEach «por dentro» sería algo así:
Fíjate que el «consumer» que le pasamos como parámetro es el que realmente hace el trabajo. forEach solo se dedica a «recorrer» el stream (es decir, la lista) y aplicar a cada elemento de esa lista el método «accept» del consumer.
De esta manera nos ahorramos recorrer «a mano» la lista de valores, ya que dejamos en manos de forEach esta tarea. Nosotros nos encargamos sólo de decirle qué hacer con cada elemento, usando un Consumer.
Y hasta aquí el artículo de hoy, en el siguiente veremos más aplicaciones de los Streams y profundizaremos en las otras interfaces funcionales: Supplier, Function y Predicate.
Un saludo y feliz semana!