Streams
Los Streams fueron introducidos en Java 8 para abrir la puerta a la programación funcional al igual que con las expresiones lambda. No hay que confundirlos con los streams de entrada/salida, como un buffer stream de entrada o un fichero de salida.
Según Oracle:
Note
A stream is a sequence of elements supporting sequential and parallel aggregate operations.
La API Stream
permite manipular las colecciones como nunca antes. Nos permite realizar operaciones sobre la colección, como por ejemplo, buscar, filtrar, reordenar, etc. Pero no nos permite manipular los elementos individualmente en el flujo, sino que se trata el flujo como un todo, a menudo agregando o reduciendo los datos, o quizás contando o agrupando elementos de alguna manera.
Con Streams podemos utilizar cualquier clase que implemente la interfaz Collection
como si fuese un Stream
con la ventaja que nos ofrecen las expresiones lambda.
Con streams hay que tener el cuenta que la fuente o colección que utilicemos no se puede modificar y no debe afectar al estado de la misma. Cada operación dentro del stream debe verse como una operación independiente que opera sobre el argumento (colección).
A través del API Stream podemos trabajar sobre colecciones como si estuviéramos realizando sentencias SQL pero de una manera limpia y clara, evitando bucles y algoritmos que ralentizan los programas e incluso hacen que el código se torne inmanejable.
Cada operación del stream debe verse como un paso independiente, es decir, no se puede usar variables intermedias.
Warning
Streams are lazy! Es decir, se inicia con la operación terminal, y los elementos de origen se consumen sólo cuando es necesario.
¿Cuándo querría utilizar Streams en vez de colecciones?
Los Streams son una interesante incorporación a Java, ya que me aportan varias ventajas:
- Hacen que el código para procesar los datos sea uniforme, conciso y legible. Tiene una forma similar a SQL.
- Cuando se trabaja con grances colecciones, los flujos paralelos proporcionan una ventaja de rendimiento.
Partes de un Stream
De forma genérica existen 3 partes que componen un Stream:
- Un Stream funciona a partir de una lista o colección, que también se la conoce como la fuente de donde obtienen información.
- Operaciones intermedias como por ejemplo el método filter, que permite hacer una selección a partir de un predicado.
- Operaciones terminales, como por ejemplo los métodos max, min, forEach, findFirst etc.
La fuente proporciona los elementos a la tubería.
Funciones de Stream
Operaciones intermedias
Las operaciones intermedias obtienen elementos uno por uno y los procesan. Todas las operaciones intermedias son perezosas (lazy) y, como resultado, ninguna operación tendrá ningún efecto hasta que la tubería comience a funcionar.
Operaciones intermedias que tienen un efecto en el tamaño del stream resultante
Tipo de retorno | Operación | Descripción |
---|---|---|
Stream |
distinct() | Elimina los valores duplicados del Stream. |
Stream |
filter(Predicate) | Los elementos que coinciden con el filtro de Predicate se mantienen en el stream de salida para las operaciones. |
Stream |
takeWhile(Predicate) | Similar a filter. Con la diferencia de que la primera vez que se evalúa a falsa la condición, deja de comprobar el resto de elementos. |
Stream |
dropWhile(Predicate) | Eliminará o filtrará cualquier elemento mientras coincida con la condición del Predicate. Cuando la condición se evalúa como falso la primera vez, la condición ya no se comprueba. |
Stream |
limit(long maxSize) | Reduce el stream al tamaño especificados en el parámetro. |
Stream |
skip(long n) | Este metodo omite elementos, es decir, no formarán parte del stream resultante. |
Peek()
El método peek
recibe como parámetro una expresión lambda de tipo Consumer
para poder utilizar cada elemento del stream. Normalmente se utilizar para mostrar por consola el contenido del stream.
Este método existe principalmente para la depuración del programa, donde se desea ver los elementos a medida que pasan por un punto determinado en el pipeline.
peek()
también se utiliza cuando queremos alterar el estado interno de un elemento (aunque esto no es muy común).
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
Sorted()
Se utiliza para ordenar los elementos del stream. Recibe como parámetro una expresión lambda de tipo Comparator
para que podamos indicar la lógica de la ordenación.
Map()
El método map
recibe como parámetro una expresión lambda de tipo Function
, por lo que debemos especificar 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 el mismo.
La función se aplica a cada uno de los elementos del stream para realizar alguna transformación sobre cada elemento y devuelve otro Stream sobre el cual puedes seguir trabajando.
Se utiliza para modificar el contenido del stream.
map()
devuelve un stream nuevo que consta de los resultados de aplicar la función dada a los elementos del stream.
List<Integer> list = Arrays.asList(3, 6, 9, 12, 15);
//Mostramos el nuevo stream devuelto por map
list.stream().map(number -> number * 3).forEach(System.out::println);
//[9 18 27 36 45]
FlatMap
Cuando nos encontramos con estructuras más complejas, como por ejemplo una lista con otra lista, trabajar con map()
no es suficiente, por ello, utilizamos flatMap()
que lo que hace es "aplanar" listas anidadas y quedarnos con un stream plano.
Es una función que recibe una entrada y devuelve varias salidas para esa entrada. Esa es la diferencia con respecto a map()
que recibe solo un parámetro de entrada y devuelve una salida.
flatMap()
es una operación intermedia y devuelve un nuevo Stream.
Devuelve un Stream que consiste en los resultados de reemplazar cada elemento del stream dado con el contenido de un stream mapeado producido al aplicar la función de mapeo provista a cada elemento.
La función de mapeo utilizada para la transformación en flatMap()
es una función sin estado y solo devuelve una secuencia de nuevos valores.
En el siguiente ejemplo el programa usa la operación flatMap()
para convertir una lista de una lista List<List<Integer>>
a una lista List<Integer>
.
List<Integer> list1 = Arrays.asList(1,2,3);
List<Integer> list2 = Arrays.asList(4,5,6);
List<Integer> list3 = Arrays.asList(7,8,9);
List<List<Integer>> listOfLists = Arrays.asList(list1, list2, list3);
List<Integer> listOfAllIntegers = listOfLists.stream()
.flatMap(x -> x.stream())
.collect(Collectors.toList());
System.out.println(listOfAllIntegers);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]
Operaciones terminales
Las operaciones terminales significan el final del ciclo de vida del steam. Lo más importante para nuestro escenario es que inician el trabajo en la tubería.
forEach()
Recorremos cada elemento del stream para realizar alguna acción con él. Como bien sabemos recibe como parámetro una exprsión lambda de tipo Consumer
.
collect()
Es una operación terminal, se utiliza para indicar el tipo de colección en la que se devolverá el resultado final de todas las operaciones realizadas en el stream.
List<String> lista = Arrays.asList("Texto1", "Texto2");
Set<String> set = lista.stream().collect(Collectors.toSet());
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 devovler ningún elemento del stream.
toArray()
Con este método se puede convertir cualquier tipo de Collection
en un array de forma sencilla.
min()
Con min
se obtiene el elemento del stream con el valor mínimo calculado a partir de una expresión lambda de tipo Comparator
que indicamos 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.
max()
Con max
se obtiene el elemento del stream con el valor máximo calculado a partir de una expresión lambda de tipo Comparator
que indicamos como parámetro de la expresión.
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.
anyMatch()
El método anyMatch
devuelve verdadero si al menos un elemento satisface la condición proporcionada por el predicado; en caso contrario, es falso. No evalúa el predicado sobre todos los elementos si no es necesario para determinar el resultado. El método devuelve verdadero tan pronto como se encuentra el primer elemento coincidente.
Si la secuencia está vacía, se devuelve falso y el predicado no se evalúa.
Sintaxis |
---|
boolean anyMatch(Predicate<? super T> predicate) |
Ventajas de Streams
- Código conciso y más limpio.
- Programación funcional: se programa escribiendo el "qué" en lugar del "cómo" para que sea comprensible de un vistazo.
- Puede leer y comprender fácilmente el código que tiene una serie de operaciones complejas.
- Ejecutar tan rápido como bucles for (o más rápido con operaciones paralelas)
- Ideal para listas grandes.
Desventajas
- Excesivo para pequeñas colecciones.
- Difícil de aprender si está acostumbrado a la codificación de estilo imperativo tradicional