Si aprendiste a programar con C, C++, Pascal, o las primeras versiones de Java y sigues dedicado a la programación es posible que hayas notado que desde hace ya algunos años han aparecido conceptos “extraños” relacionados con la programación funcional como pueden ser las lambdas. Es posible que si eres de esa “vieja escuela” no estés muy conforme con ellas ya que parecen complicadas y poco claras.
En este post vamos a tratar de quitar ese estigma para demostrar que bien usadas es una herramienta muy potente para reducir líneas de código y esfuerzo.
1. ¿Qué es una lambda??
Lo primero es tratar de definir que es una expresión lambda. Un lambda es un concepto relacionado con la programación funcional que se definió en Java 8 para tratar de dotar a Java de un paradigma funcional que difiere del paradigma de la programación imperativa.
2. ¿Qué es Java Stream?
En la versión 8 de Java se incluyó un API que facilitaba en gran medida el trabajo con colecciones. Este nos permite realizar operaciones sobre la colección, como por ejemplo buscar, filtrar, reordenar, reducir, etc…
Ahora vamos a ver algunos de los métodos que nos ofrece (aunque existen muchos otros que podéis utilizar y que seguramente os sean de mucha utilidad).
Si quieres saber más sobre Java Stream, aquí tienes varios post que seguro son muy útiles ?
¿El fin de Java se encuentra tras la versión 11?
3. Mi primera lambda… “forEach”
La primera lambda que vamos a utilizar es una de las más básicas y que seguramente muchos ya conocéis. Se trata del método forEach que encontramos en la mayoría de las clases que nos permiten almacenar colecciones (Iterable, Map…).
Este método nos permite recorrer las distintas colecciones de manera sencilla sin necesidad de utilizar los bucles de la programación imperativa (for, while, do while…).
List<String> colors = Arrays.asList("Red","White","Black","Blue","Yellow");
colors.forEach(color -> {
cars.add(Car.builder().brand("Volvo").model("XC90").color(color).date(LocalDate.now())
.id(RandomStringUtils.random(7, true, true)).build());
});
En el código de arriba vemos como a partir de una lista de colores, recorriéndola apoyándonos en el método forEach, añadimos un coche con el color leído en una lista.
Para usar el forEach directamente vale con generar la siguiente estructura lambda.
colors.forEach (<variable> -> <actions>);
Esto iterará por la lista tantas veces como elementos contenga y en <variable> introducirá el valor de esa ocurrencia. Esa variable tendrá el tipo de dato que contenga la lista, en este caso es un String pero podría ser un dato compuesto declarado en nuestra aplicación.
Este uso del forEach suele ser muy útil para recorrer mapas, como podemos ver en el siguiente fragmento de código.
HashMap<String,String> map = new HashMap<>(); map.put("Volvo","XC90"); map.put("Seat","Leon"); map.put("Fiat","Punto"); map.put("Mercedes","CLA"); map.forEach((k,v)-> cars.add(Car.builder().brand(k).model(v) .color("White").date(LocalDate.now()) .id(RandomStringUtils.random(7, true, true)).build()) );
En este caso tenemos un mapa de marcas y modelos que recorremos para añadir coches en nuestra lista. Si nos fijamos la diferencia con el ejemplo anterior es que podemos declarar dos variables en el inicio de la lambda, que corresponden con la key y el value del mapa.
map.forEach( (<key>,<value>) -> <actions>);
De esta manera ahorramos bastante código para recorrer el mapa que si lo tuviéramos que recorrer utilizando programación imperativa, apoyándonos en el método entrySet para obtener el set de elementos que contiene el mapa, y los métodos getKey y getValue.
Una vez hemos visto una de las más simples vamos a comenzar a trabajar con Stream que personalmente es donde creo que más beneficio obtenemos.
4. Encontrar elementos con Stream
Imaginemos que queremos comprobar que existen o no existen determinados coches en la lista que hemos generado con los ejemplos anteriores.
Si quisiéramos por ejemplo comprobar si existen coches grises en la lista tendríamos dos opciones. Mediante programación imperativa, podríamos generarnos un método findCar.
private boolean findCar(List<Car> carList, String color){ for (Car next: carList){ if (next.getColor().equals(color)){ return true; } } return false; }
Este método recibiría la lista de elementos y el color a filtrar, el método recorre la lista para localizar si existen elementos que cumplan ese criterio de búsqueda.
Una vez tengamos ese método generado, bastaría con invocarlo tantas veces como lo necesitemos del siguiente modo.
findCar(cars,"Grey");
Ahora vamos a ver como resultaría al utilizar Stream, en este caso directamente podríamos poner la sentencia en el siguiente formato usando lambdas.
cars.stream().anyMatch(car -> car.getColor().equals("Grey"));
Esta sentencia lo que nos busca es si existe algún registro en la lista cars cuyo color sea igual a Grey devolviendo un booleano.
Como podemos notar, en este último caso estamos evitando la generación de bastantes líneas de código.
Stream nos ofrece la posibilidad de encontrar positivos o negativos, es decir, preguntar si un elemento existe (anyMatch) o si un elemento no existe en la colección (noneMatch) la sintaxis sería la misma.
stream().noneMatch(<record> -> <condition>)
cars.stream().noneMatch(car -> car.getColor().equals("Grey"));
En este caso la partícula <record> es análoga al que hemos visto anteriormente con el método forEach, corresponde al elemento de la lista (en este caso además es una clase nuestra y cómo podemos ver, podremos acceder directamente a sus métodos).
La partícula <condition> se establece la condición que queremos se valide, en este caso hemos recuperado el atributo color para compararlo con un color específico, pero podríamos realizar cualquier condición o mezcla de ellas.
Por ejemplo si quisiéramos encontrar todos los coches de la marca Volvo y Blancos bastaría con ejecutar lo siguiente:
cars.stream().noneMatch(car -> car.getColor().equals("Grey") && car.getBrand().equals("Volvo"));
En los casos en que la condición sea compleja, podemos extraer la condición a un nuevo método e invocara este en la sentencia lambda.
private boolean applyCriteria(Car car) { return car.getColor().equals("Grey") && car.getBrand().equals("Volvo"); }
cars.stream().noneMatch(car -> applyCriteria(car));
Este mismo proceso podemos hacerlo con todas las funciones que nos ofrece Stream.
5. Filtrar elementos y operaciones adicionales con Stream
Además de lo que hemos visto hasta ahora, el Stream nos permite filtrar resultados de una lista y realizar determinadas operaciones sobre los elementos resultantes de ese filtro.
Por ejemplo imaginemos que queremos filtrar los coches que son blancos para aplicarles una bonificación en su seguro.
Lo podríamos realizar utilizando el método filter.
cars.stream().filter(x -> x.getColor().equals("White")).forEach(car -> improveInsurance(car));
Utilizando filter, generamos otro objeto Stream que nos permite realizar otra función sobre él, en este caso hemos utilizado el forEach para realizar una llamada al método para realizar la bonificación.
Cabe destacar que sobre el objeto Stream generado por el método filter podemos realizar muchas operaciones:
- Contar el número de elementos que se obtienen, para ello utilizaremos el método count
cars.stream().filter(x -> x.getColor().equals("White")).count();
- Generar una nueva lista con los resultados obtenidos, en este caso utilizaremos el método collect
List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).collect(Collectors.toList());
- Generar una nueva lista con las matrículas de los coches que hemos filtrado, para ello utilizamos el método map y luego el método collect
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(x -> x.getId()).collect(Collectors.toList());
En este caso estamos mapeando el campo Id a través del método getId a una nueva lista en el que guardaremos solo las matrículas. Esta anotación que hemos utilizado en el map podemos simplificarla con una referencia al método del siguiente modo Car::getId
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(Car::getId).collect(Collectors.toList());
- Generar una nueva lista eliminando duplicados, por ejemplo, imaginemos que queremos obtener todas las marcas que tienen un coche blanco, en este caso es posible que ese filtro nos de registros duplicados, esos registros se pueden eliminar del siguiente modo
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(Car::getBrand).distinct().collect(Collectors.toList());
- Ordenar el filtro resultante utilizando uno de los campos de la clase, en este caso utilizaremos el método sorted apoyándonos en la clase Comparator en la cual estableceremos cual es el campo sobre el que vamos a realizar la ordenación, en este caso también utilizaremos la referencia al método.
List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).sorted(Comparator.comparing(Car::getId)).collect(Collectors.toList());
En definitiva se pueden realizar un montón de operaciones de manera simple utilizando Stream, como hemos comentado estas quizás sean las más básicas, pero existen otros métodos como reduce, limit, flatMap, skip, peak,… que nos ofrecen más posibles funcionalidades sobre el objeto Stream.
Conclusión?
Es cierto que hay quien puede pensar que al eliminar líneas de código estamos aumentando la complejidad del SW y su mantenimiento, pero al final son métodos sencillos que pueden aportar una rebaja sustancial del tiempo y de las líneas necesarias para realizar una funcionalidad.
Pero si tienes curiosidad por este nuevo modelo de programación esto puede servirte como un pequeño aperitivo.
Santander Global Tech es la empresa de tecnología global, parte de la división de Technology and Operations (T&O) de Santander. Con más de 2.000 empleados y basada en Madrid, trabajamos para convertir al Santander en una plataforma abierta de servicios financieros.
¿Eres experto en programación y quieres unirte a este equipazo? Mira las posiciones que tenemos abiertas aquí y Be Tech! with Santander ?
Síguenos en LinkedIn y en Instagram.