Uso básico de Streams en Java
Java

Uso básico de Streams en Java

Silverio Martínez García
Silverio Martínez García

Desde la versión 8 de Java podemos utilizar cualquier clase que implemente la interfaz Collection como si fuese un Stream con las ventajas que nos ofrece la programación funcional y las Expresiones Lambda.

Una vez que te acostumbres a utilizar los streams ya no querrás volver a hacer bucles del tipo 'for(Class var1: listType){}'

A continuación voy a poner unos ejemplos de los usos más frecuentes con streams.
Puedes descargar el código fuente desde GitHub.

Preparándonos para picar código:

Vamos a crear una clase 'Book' que será nuestro model y un repositorio 'BookRepository' que devolverá una lista de Books para poder utilizar en nuestros tests. Así mostramos como funciona cada función por separado y con su correspondiente comprobación del resultado final.

Book.java

BookRepository.java

StreamsUnitTest.java

Funciones del Stream:

Ahora veamos uno por uno cada test para explicar un poco como funciona cada función del stream utilizada.

Collect():

'Collect()' se utiliza para indicar el tipo de collection en la que se devolverá el resultado final de todas las operaciones realizadas sobre el stream.
En este ejemplo no se realiza ninguna operación sobre el stream y simplemente se convierte el contenido a un List y a un Set.
En el caso del Set se comprueba como no se guardan los valores repetidos del 'stream' debido a la propia naturaleza del Set que impide guardar valores duplicados.

Peek():

El método 'peek()' recibe como parámetro una lambda de tipo 'Consumer' para poder utilizar y/o modificar cada elemento del stream.
Normalmente se utiliza para mostrar por consola el contenido del stream en cada momento.

También se puede modificar el valor de algún campo del objeto 'Book' como en nuestro ejemplo pero no es muy aconsejable, ya que en la programación funcional se aconseja utilizar objetos inmutables.
No es una buena práctica cambiar el valor del objeto directamente.
En este caso en particular sería mejor utilizar el método 'map()' que veremos más adelante devolviendo una copia modificada de cada elemento de la lista.

Map():

El método 'map()' recibe como parámetro una lambda de tipo 'Function' por lo que debemos indicar una función que recibe como parámetro de entrada cada elemento del stream y devuelve un objeto que puede ser un tipo de dato distinto o del mismo.

Se utiliza para modificar el contenido del stream a partir de un punto determinado del flujo.

En nuestro ejemplo el 'stream' comienza con un flujo de objetos de tipo 'Book' y a partir del 'map()' se convierte en un flujo de tipo 'String' al devolver el título de cada libro.

Se utiliza 'peek()' para mostrar por consola los valores devueltos por 'map()' y finalmente se recolecta el resultado como una lista.

ForEach():

Con 'forEach()' recorremos cada elemento del stream para realizar alguna acción con él. Recibe como parámetro una lambda de tipo 'Consumer'.

Si nos fijamos bien 'forEach()' es un método que no devuelve ningún valor.
A diferencia del resto de funciones vistas, 'forEach()' es una operación de terminación, es decir, no se pueden seguir realizando operaciones encadenadas con el mismo stream.

En nuestro primer ejemplo recorremos el stream de libros para modificar su precio y mostramos por consola su nuevo valor.

En el segundo ejemplo primero obtenemos los títulos de los libros por medio de la función 'map()' y utilizamos 'forEach()' para comprobar el resultado del test.

Filter():

Con 'filter()' como su nombre indica, lo que hacemos es filtrar de todos los elementos del stream solo aquellos que cumplan una determinada condición.
Recibe como parámetro una lambda de tipo 'Predicate' la cual debe devolver 'true' solo en aquellos elementos que seguirán en el stream y 'false' para aquellos que se deben eliminar.

En nuestro ejemplo usamos 'filter' para seleccionar de todos los libros solo aquellos cuyo precio cumpla la condición indicada.
También usamos 'peek()' a continuación para mostrar por consola los libros seleccionados.

FindFirst():

La función 'findFirst()' se utiliza para devolver el primer elemento encontrado del 'stream'. Se suele utilizar en combinación con otras funciones cuando hay que seleccionar un único valor del Stream que cumpla determinadas condiciones.

'findFirst()' devuelve un objeto de tipo 'Optional' para poder indicar un valor por defecto en caso de que no se pueda devolver ningún elemento del 'stream'.

En el ejemplo se filtran los libros por su precio, a continuación se muestran por consola los libros filtrados y finalmente se selecciona el primero que cumpla la condición.
En caso de que ningún libro cumpliese la condición del 'filter()' indicamos que se devuelva como valor 'null'.

Este es un buen ejemplo para observar una característica propia del funcionamiento de los 'streams', que es la 'LazyEvaluation'.
Las funciones del 'stream' se van ejecutando una tras otra por cada elemento (libro) y luego se pasa al siguiente elemento sobre el cual se vuelven a ejecutar todas las fuciones y así sucesivamente por cada elemento del stream.

En nuestro ejemplo hay 2 libros que cumplen la condición del 'filter()', pero si lo ejecutamos observamos que en el 'peek()' solo se muestra por consola el primero de ellos.
Esto es debido a que en cuanto el primer libro que cumple la condición del 'filter()' llega al 'findFirst()' el stream se finaliza devolviendo dicho elemento, con lo cual sobre el siguiente libro ya no se llega a ejecutar el correspondiente 'filter()' ni el resto de funciones del 'stream'.

ToArray():

Con 'toArray()' podemos convertir cualquier tipo de 'Collection' en un 'array' de forma sencilla.

Las 2 primeras líneas del ejemplo sirven de recordatorio para ver como se puede convertir una lista en un 'array' sin utilizar 'streams'.

A continuación se muestra como convertir un 'stream' (obtenido a partir de cualquier 'Collection') en un 'array'.

Y por último se muestra como obtener un 'stream' partir de un 'array' y se utiliza para mostrar por consola sus valores.

Sorted():

'sorted()' se utiliza para ordenar los elementos del 'stream'.
Recibe como parámetro una lambda de tipo 'Comparator' para que podamos indicar la lógica de ordenación.

En nuestro ejemplo estamos ordenando los libros por su campo 'author' en orden ascendente sin tener en cuenta las mayúsculas y minúsculas.

Min():

Con 'min()' se obtiene el elemento del 'stream' con el valor mínimo calculado a partir de la lambda de tipo 'Comparator' que indiquemos como parámetro.

'min()' devuelve un objeto de tipo 'Optional' para poder indicar un valor por defecto en caso de que no se pueda devolver ningún elemento del 'stream'.

En el ejemplo se obtiene el libro con el menor precio.
Se indica en el objeto 'Optional' devuelto por 'min()' que en caso de no encontrar ningún elemento se devuelva 'null'.

Max():

Con 'max()' se obtiene el elemento del 'stream' con el valor máximo calculado a partir de la lambda de tipo 'Comparator' que indiquemos como parámetro.

'max()' devuelve un objeto de tipo 'Optional' para poder indicar un valor por defecto en caso de que no se pueda devolver ningún elemento del 'stream'.

En el ejemplo se obtiene el libro con el mayor precio.
Se indica en el objeto 'Optional' devuelto por 'max()' que en caso de no encontrar ningún elemento se devuelva 'null'.

Distinct():

Con 'distinct()' se seleccionan los elementos distintos dentro del 'stream' eliminando los duplicados.

En nuestro ejemplo primero añadimos un nuevo libro a la lista con un nombre repetido. A continuación convertimos la lista en un 'stream' y utilizamos 'map()' para obtener los títulos de los libros y a continuación con 'distinct()' se excluyen los valores duplicados.

Hay que tener en cuenta que si usase 'distinct' directamente sobre el objeto 'Book' del stream, habría que implementar su correspondiente método 'equals()' que es el que se utiliza para comparar la igualdad de los elementos entre sí.

allMatch(), anyMath() y noneMatch():

Las funciones 'allMatch()', 'anyMatch()' y 'noneMatch()' devuelven un 'boolean' después de ejecutar la lambda de tipo 'Predicate' que reciben como parámetro sobre cada elemento del 'stream'.

  • 'allMatch()': Devuelve true en caso de que la condición de la lambda se cumpla para todos los elementos del 'stream' y false en caso de que algún elemento no cumpla la condición.
    Se utiliza evaluación por cortocircuito, es decir, en cuanto la condición no se cumple para un elemento ya no se sigue comparando con el resto y 'allMatch()' devolverá false.

En nuestro ejemplo comprobamos que todos los elementos del 'stream' sean par (el resto de la división entre 2 debe dar O como resultado). Como existe un elemento que no es par, el resultado de 'allMatch()' es false.

  • 'anyMatch()': Devuelve true en caso de que la condición de la lambda se cumpla para algún elemento del 'stream' y false si ningún elemento cumple la condición.
    También se utiliza evaluación por cortocircuito, es decir, en cuanto la condición se cumpla para un elemento ya no se sigue comparando con el resto y 'anyMatch' devolverá true.

En nuestro ejemplo comprobamos que algún elemento del 'stream' sea par. Como el primer elemento seleccionado ya es par, 'anyMatch()' devolverá true y no seguirá evaluando el resto de elementos.

  • 'noneMatch()': Devuelve true en caso de que ningún elemento del 'stream' cumpla la condición de la lambda y false en caso de que algún elemento cumpla la condición.
    También se utiliza evaluación por cortocircuito, por lo que en cuanto se encuentre un elemento que cumpla la condición ya no se sigue comparando con el resto y 'noneMatch()' devolverá false.

En nuestro ejemplo comprobamos que ningún elemento del 'stream' sea múltiplo de 3, como sí que existe un elemento que cumple dicha condición, 'noneMatch()' devolverá false y no seguirá evuluando el resto de elementos.

Conclusión:
En este artículo hemos visto las funciones más comunes para utilizar con los streams de Java8 y versiones posteriores.

Existen más funciones y características que no se cubren en este artículo pero a continuación dejo un par de enlaces para seguir investigando sobre su uso.

Espero que este artículo te haya servido de utilidad y a partir de ahora empieces a utilizar los streams y las lambda para ser un ATopeCoder de verdad :)

Nos vemos.

Enlaces de interés: