Streams en Java
Published: 2023-04-14 02:25:06

## 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:

```java
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.
}
```
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:

```java
// 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);
```

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:

```java
// 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);
```

### 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:

```java
integers.stream().forEach(item -> System.out.println(item));
```

## ¿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.
```java
// 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);
```
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:
```java
integers.stream().
    filter(item -> item%2 == 0).
    filter(item-> item%3==0).
    count();
```
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:
```java
integers.stream().parallel().
    filter(item -> item%2 == 0).
    filter(item-> item%3==0).
    count();
```

## Conclusión
En este post, repasamos los conceptos sobre Streams apicables a Java, pero tambien [se pueden hacer en Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). Los stream son una herramienta muy útil para escribir código performante, pero además, para poder hacerlo más fácilmente legibles.