Contents

Java 8: Streams

Junto con las expresiones lambda, los Streams son una de las funcionalidades más relevantes de Java 8, y trae consigo una nueva forma de trabajar. Mediante una capa de abstracción, los Streams nos permiten definir la lógica de negocio como un conjunto de funciones que se ejecutan de forma anidada.

De este modo, podemos trabajar con colecciones utilizando el paradigma de programación funcional, que nos permite definir las funciones a ejecutar de una forma mucho más clara y, en cierto modo, lo más parecida posible a como lo haríamos las personas en una situación real.

El siguiente código muestra la lógica de un programa que filtra una lista de enteros, y devuelve el valor más pequeño de la lista, sin tener en cuenta los números pares:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void main(String\[\] args) {
    List<Integer> integers = Arrays.asList(3, 12, 21, 4, 2, 7, 5);
    Integer min = integers.get(0);

    for (Integer next : integers) {
        if (next % 2 == 0)
            continue;
        if (next < min) {
            min = next;
        }
    }

    System.out.println(min);
}

Ahora veremos cómo se escribiría este mismo código utilizando Streams.

1
2
3
4
5
6
7
8
9
public static void main(String\[\] args) {
    List<Integer> integers = Arrays.asList(3, 12, 21, 4, 2, 7, 5);

    Optional<Integer> min = integers.stream()
            .filter(integer -> integer % 2 != 0)
            .min(Integer::compareTo);

    System.out.println(min.get());
}

Como se puede observar, el código queda mucho más claro, ya que se organiza por funciones anidadas, que se ejecutan como un flujo de trabajo, generando nuevos _Stream_s a medida que avanzan en la cadena de ejecución.

Elementos de un Stream

Como ya he comentado, los Streams son abstracciones que nos permiten definir operaciones agregadas sobre una fuente de datos utilizando funciones.

Los Streams se componen de tres partes:

// Fuente de datos: 1 Stream.of(“Some, Comma, Separated, Values”)

// Operaciones intermedias: 0 -> N .flatMap(string -> Stream.of(string.split(","))) .map(string -> string.trim())

//Operación terminal: 1 .forEach(System.out::println);

https://lh5.googleusercontent.com/59vUmdkejTGzsy-3q7TQxOsL33z8vVkY1ET8u0IFNGJhaSyy78i2Th7jGW1_vc1c7k_Rr6g7ljPeO5DPzLtTJIPkIrbRb-LrZxzlLVnePevqjCsG-VzIuq4IvOQtQ9q3v1KVl7RVzZeLgNuEiA

Fuentes de datos

La fuente de datos consiste en una colección de datos que pueden ser tratados como una cadena de valores.

Java ofrece distintas fuentes de Streams para ciertas clases:

Collection.stream()JarFile.stream()
ZipFile.stream()
Collection.parallelStream()BufferedReader.lines()
Arrays.stream()Pattern.splitAsStream()
Files.find(Path, BiPredicate, FileVisitOption)CharSequence.chars()
Files.list(Path)CharSequence.codePoints()
Files.lines(Path)BitSet.stream()
Files.walk(Path, FileVisitOption)Random().Ints()
Random().Doubles()
Random().Longs()

También podemos utilizar métodos estáticos para crear Streams a partir de datos existentes:

Stream.concat(Stream, Stream)Concatena dos Streams existentes y devuelve el Stream resultante.
Stream.of(T… values)Crea un Stream a partir de los valores de entrada.
IntStream.range(int, int)Stream de enteros entre los dos valores dados como parámetros de entrada.
Stream.generate(IntSuppliert)Stream generado por un proveedor de resultados.
Ejemplo:
Stream.generate(new Random()::nextInt)

Stream.iterate(int, IntUnaryOperator)El método Iterate() utiliza un valor como semilla para generar el Stream.

Operaciones intermedias

Las operaciones intermedias son operaciones que se aplican sobre la fuente de Stream definida. Estas operaciones no se ejecutan hasta que no se incluye una operación terminal en la secuencia de operaciones, que es la que causa que la cadena de operaciones se ejecute.

La mayoría de las operaciones intermedias reciben un parámetro (interfaz funcional) que define su comportamiento, generalmente utilizando expresiones lambda. Como resultado, todas las operaciones intermedias devuelven un Stream, para que se puedan seguir concatenando operaciones.

El comportamiento definido para estas operaciones debe ser no interferentes sobre la ejecución del Stream, esto significa que la ejecución de la operación actual no debe afectar al resto. También deben ser stateless, es decir, debe tratar cada ejecución independientemente, sin relacionar las ejecuciones entre sí.

Estos dos patrones de comportamiento permite que un Stream pueda ser ejecutado de forma secuencial o paralela, sin necesidad de modificaciones, y definiendo su comportamiento en tiempo de ejecución.

Existen múltiples operaciones intermedias, a continuación veremos las más frecuentes.

Operaciones de filtrado y mapeado

distinct()Devuelve un Stream sin elementos duplicados.
filter(Predicate)Devuelve un Stream únicamente con los elementos que devuelven true al Predicate definido.
map(Function)Devuelve un Stream en el cual se le aplica la función definida a cada uno de los elementos del Stream original.
mapToInt(Function)
Devuelve un Stream de enteros aplicando la función definida a cada uno de los elementos del Stream original.
mapToDouble(Function)Devuelve un Stream de Doubles aplicando la función definida a cada uno de los elementos del Stream original.
mapToLong(Function)Devuelve un Stream de Longs aplicando la función definida a cada uno de los elementos del Stream original.

Además de las operaciones anteriores, existe un tipo de mapeado que devuelve múltiples resultados por cada valor procesado, este tipo de mapeado se denomina FlatMap. En la siguiente imagen se puede distinguir fácilmente la diferencia entre los Maps y los FlatMaps.

https://lh6.googleusercontent.com/5SQZyuBcZlUXKJC2ukGhlqpyCRQm_y4QNJ4MpnQ8Y6VrUE9oayRB2JQrHE-iwayF8wXgUtUvDf2m4J_UtwGn7epFh6fOIWR_8EZTQfpGcRO595eZrywzflt-VxfWr0ARdVvdERwL-Ws3pJEB_w

Un ejemplo práctico sería el siguiente, en el cual creamos un Stream a partir de un String, haciendo un split por la coma utilizando el método FlatMap.

Este método nos genera un nuevo Stream a partir de cada uno de los resultados obtenidos que podemos seguir procesando.

1
2
3
4
5
6
7
public static void main(String\[\] args) {

    Stream.of("Some, Comma, Separated, Values")
                .flatMap(string -> Stream.of(string.split(",")))
                .map(string -> string.trim())
                .forEach(System.out::println);
}

Operaciones de restricción de tamaño

skip(Long n)Devuelve un Stream que ignora los primeros n elementos de la fuente de datos.
limit(Long n)Devuelve un Stream que contiene los primeros n elementos de la fuente de datos.

Operaciones de ordenación y desordenación

sorted()Devuelve un Stream ordenado por ordenación natural. La ordenación natural es el orden el que un objeto primitivo debe ordenarse en una colección.
sorter(Comparator)Devuelve un Stream ordenado mediante el Comparator recibido como parámetro de entrada.
Unordered()Devuelve un Stream desordenado. Puede mejorar la eficiencia de otras operaciones como distinct() o groupingBy().

Observando los elementos de un Stream

peek(Consumer)Devuelve un Stream idéntico al Stream del data source, aplicando la acción del Consumer a cada uno de los elementos.

El método peek() no debería modificar los elementos del Stream.

Generalmente utilizado para debugear.

Operaciones terminales

Las operaciones terminales son las que terminan el pipeline de operaciones de un Stream y activan su ejecución. Un Stream solo se ejecutará cuando el pipeline incluya una de estas operaciones.

Al interpretar cómo se debe ejecutar la secuencia de operaciones del Stream, se aplican procesos de optimización para mergear o rechazar operaciones innecesarias, eliminar operaciones redundantes o ejecutar el stream en paralelo si fuera necesario.

Las operaciones terminales generan un valor o una acción como resultado, esto hace que este tipo de operaciones cierren la cadena de ejecución, ya que no devuelve un Stream.

Las operaciones terminales más comunes son las siguientes:

Operaciones condicionales

Optional<T> findFirst(Predicate)Devuelve el primer elemento del Stream que coincida con la condición del predicado, o un Optional vacío si no se cumple la condición.
Optional<T> findAny(Predicate)Devuelve un elemento aleatorio del Stream que coincida con la condición del predicado, o un Optional vacío si no se cumple la condición. Pensado para ofrecer un mayor rendimiento en operaciones paralelas.
Boolean allMatch(Predicate)Devuelve true si la condición del predicado se cumple para todos los elementos del Stream, false para el resto de casos.
Boolean anyMatch(Predicate)Devuelve true si la condición del predicado se cumple para cualquier elemento del Stream, false para el resto de casos.
Boolean noneMatch(Predicate)Devuelve true si la condición del predicado no se cumple para ningúnelemento del Stream, false para el resto de casos.

Operaciones que devuelve colecciones

collect(Collector)Devuelve el Stream en forma de colección del tipo que definamos. Podemos utilizar la clase de utilidad Collectors, que nos ofrece varias implementaciones de la interfaz Collector.

toArray()
Devuelve los elementos del Stream a modo de Array.

Operaciones que devuelve resultados numéricos

count()Devuelve un entero con el número de elementos que contiene el Stream.
max(Comparator)Devuelve el valor más grande que contenga el Stream. Podemos definir el comparador que vamos a utilizar.
min(Comparator)Devuelve el valor más pequeño que contenga el Stream. Podemos definir el comparador que vamos a utilizar.
average()Devuelve la media aritmética del Stream.
sum()Devuelve la suma de los elementos del Stream.

Operaciones de iteración

forEach(Consumer)Ejecuta la acción definida en el consumidor a cada elemento del Stream.
forEach(Consumer)Realiza la misma acción que forEach() pero asegurando que el orden de ejecución se mantiene cuando se usa con Streams ejecutados de forma paralela.

Operaciones de reducción

reduce(BinaryOperator accumulator)Realiza una reducción en el Stream utilizando un BinaryOperator.

El BinaryOperator recibe dos parámetros de entrada: el primero es el valor ya reducido, y el segundo es el valor a reducir.
Esta acción se aplica a cada uno de los elementos del Stream.