DEV Community

Cover image for Streams en Java
Gino Luraschi
Gino Luraschi

Posted on

Streams en Java

Introducción

Para salir un poco de Go, hoy vamos a ver algo que no está incluido en la lib de Go, pero que es una buena herramienta a utilizar en lenguajes de programación cómo Java, Javascript, Kotlin entre otros, esta herramienta son los streams. Estos se utilizan para manipular colecciones de datos como los Arrays de la manera en que lo hace una arquitectura pipeline o ETL (extract, transform and load data).

De donde vienen?

Los streams provienen de la programación funcional, donde se los definen como:

Un paradigma de programación declarativa basado en el uso de verdaderas funciones matemáticas. En este estilo de programación las funciones son ciudadanas de primera clase, porque sus expresiones pueden ser asignadas a variables como se haría con cualquier otro valor; además de que pueden crearse funciones de orden superior (o sea tomar una o más funciones como entrada, o devolver una función como salida).

En resumen, la programación funcional se reduce al uso de funciones para manipular datos. A continuación veremos como hace Java para manipular distintos arreglos de datos.

Aclaración

Los streams de datos son inmutables por lo que generan nuevo streams y el array original no cambia en ningún momento.

Java

Si recorremos la API de Java para streams, nos encontramos con los métodos que podemos utilizar para la manipulación de arrays. Para eso vamos a ver los métodos más comunes.
Ante todo para esto vamos a crear un nuevo arreglo:

public static void main(String[] args) {
    // Generamos un array con los numeros de 1 al 1000 
    List<Integer> integers = new ArrayList<>();
    for (int x = 1; x < 1000; x++) integers.add(x);

    // El codigo irá aca.
}
Enter fullscreen mode Exit fullscreen mode

A partir de este mapa generamos los siguientes ejemplos.

Filter

Cuando ejecutamos el filter en java, vemos que este recibe un tipo de clase Predicate<T> que dentro tiene un método test() que devuelve un booleano, este método test es necesario para evaluar cada ítem del arreglo. Para no alarmarse de crear una nueva clase que extienda de Predicate, podemos utilizar una función lambda(o sea una función de orden superior) dentro de la función filter:

// Acá podremos filtrar nuestros valores pares
// val -> val%2 == 0 es nuestra función lambda 
// que devuelve un booleano
integers.stream().filter(val -> val%2 == 0);
Enter fullscreen mode Exit fullscreen mode

Esto retornará un stream con los ítems que cumplen la condición dentro de la función lambda.

Map

En cuanto a la función map() vemos que recibe una Function<T,R> que acepta un tipo de dato (el item del array) y devuelve un resultado, formando un nuevo array con los cambios aplicados para cada item del array inicial. Un ejemplo sería sumar un número más a cada uno de nuestros ítems:

// A cada item del mapa le sumaremos un numero
// x -> x+1 es nuestra función lambda que devuelve 
// un integer para generar nuestro array de integers
integers.stream().map(x -> x+1);
Enter fullscreen mode Exit fullscreen mode

For Each

El forEach es una de las funciones más utilizadas en Java, y significa que por cada ítem aplicaremos una acción. La característica de esta función es que no nos genera un array nuevo, por lo que es una "función terminal". Esta función recibe un Consumer<T> que solo acepta los items, pero no devuelve nada. Un ejemplo es mostrar por pantalla los ítems:

integers.stream().forEach(item -> System.out.println(item));
Enter fullscreen mode Exit fullscreen mode

¿Mejoran el código?

Si llevamos esto a un ejemplo en Java, podemos ver que la sintaxis cambia y se ve un decremento en la cantidad de líneas utilizadas para hacer lo mismo con los streams.

// For convencional
for (int i: integers){
    if (i%2 == 0) {
        int res = i+1;
        System.out.println(res);
    }
}
// Con el stream
integers.stream().
    filter(item -> item%2 == 0).
    map(item -> item +1).
    forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

La diferencia en este caso, no solo la encontramos en cuanto a las líneas de código, sino también en cuanto a la sintaxis que se puede leer de manera más corrida.

Son más eficaces?

Como mencionamos antes, el ForEach es una función terminal, pero no es la única, también encontramos el count()(para contar los elementos de un stream) o el collect(Collector)(para recolectar los elementos, por ejemplo en un nuevo array).
Estas funciones terminales, son las funciones que van a ejecutar cada una de nuestras iteraciones, a este mecanismo se lo denomina Lazy Evaluation. Veamos el ejemplo:

integers.stream().
    filter(item -> item%2 == 0).
    filter(item-> item%3==0).
    count();
Enter fullscreen mode Exit fullscreen mode

En este caso, al ejecutar el count, se va a iterar cada ítem del array, filtrando los numeros pares, y luego filtrando los divisibles por 3, item por item. A esto, se le puede agregar la ejecución en paralelo, haciéndolo más eficaz y rápido:

integers.stream().parallel().
    filter(item -> item%2 == 0).
    filter(item-> item%3==0).
    count();
Enter fullscreen mode Exit fullscreen mode

Conclusión

En este post, repasamos los conceptos sobre Streams apicables a Java, pero tambien se pueden hacer en Javascript. Los stream son una herramienta muy útil para escribir código performante, pero además, para poder hacerlo más fácilmente legibles.

Top comments (0)