Interfaces funcionales - Java 8

Gabriel Babler
Author
March 11, 2021
xx
min reading time

¿Ha pensado alguna vez en las interfaces funcionales y cómo funcionan en Java? ¡Veámoslas ahora!

En primer lugar, ¿sabe qué es una interfaz funcional?

Las interfaces funcionales son interfaces que tienen un método a implementar, es decir, un método abstracto. Esto significa que cada interfaz creada que respeta esta premisa se convierte automáticamente en una interfaz funcional.

El compilador reconoce esas interfaces y permite que estén disponibles para que los desarrolladores trabajen, por ejemplo, con expresiones lambda.

Hoy, vamos a hablar sobre las interfaces funcionales principales presentadas en JDK, que son:

  • Supplier
  • Consumidor y Biconsumidor
  • Predicado y BiPredicado
  • Función y BiFunción
  • UnaryOperator y BinaryOperator

Supplier

Al verificar su clase, podemos ver esto a continuación:

¿Qué podemos concluir con eso?

La letra T significa que es genérico (genérico significa que puede ser de cualquier tipo), y en este caso significa que la operación get(), cuando se ejecute, nos devolverá algo y puede ser de cualquier tipo.

Por otro lado, no necesitamos pasar un argumento. En resumen, lo llamamos y recibimos algo, como un proveedor (¿entendió el porqué del nombre ahora?).

Veamos un ejemplo:

Aquí estamos llamando al método generate() desde la API Stream, que necesita un Supplier para ser ejecutado.

Entonces, no estamos pasando nada al método usando los corchetes vacíos '()', luego usando lambda '->' y finalmente ejecutando el método - new Random().nextInt() - que nos devolverá algo (en este caso, un número aleatorio).

Aquí, acabamos de agregar el límite y forEach para mostrarle el resultado en la consola.

Entonces, si ejecutamos el código, obtendremos:

Así es como funciona el Supplier: no necesitamos proporcionar nada y recibimos una respuesta.

Consumidor y Biconsumidor

Revisemos su clase:

Es una interfaz simple, lo opuesto a Supplier. Recibe una variable genérica, hace algo con ella y luego no devuelve nada.

Ejemplo:

Ejemplo

Tenemos una lista de enteros y para imprimir estos números en la consola, podemos usar.forEach para ayudarnos con eso.

Recibe una variable (número) y hace algo con ella. En este caso, se imprime en la consola y no devuelve nada al usuario.

Al igual que un consumidor. Obtiene algo, hace algo y eso es todo.

BiConsumer sigue las mismas reglas pero recibe dos argumentos.

Ejemplo

En el ejemplo anterior, estamos recibiendo dos valores enteros y simplemente los imprimimos en la consola y no devolvemos nada.

Predicado y BiPredicado

Ahora, echemos un vistazo a las clases Predicate y BiPredicate:

Ejemplo

La clase Predicate tiene un método llamado test, que recibe un argumento y devuelve un booleano.

Podemos concluir que este método se utiliza para validar hipótesis. Veamos en el código:

Para este ejemplo, obtuvimos una lista de números enteros que se compone de los números: 1, 2, 3, 4 y 5.

Y nuevamente, usaremos la API Stream. Vamos a convertir nuestra lista en un Stream a través del.stream(), luego usaremos .filter(), y es en este pequeño donde ocurre la magia con el Predicate.

Nuestro filtro de método necesita un Predicate para ejecutarse y, como hemos visto antes, validará una hipótesis y nos devolverá Verdadero o Falso.

En este método, obtenemos el número y comprobamos si es divisible por 2, lo que significa que es un número par. Si es cierto, ejecutaremos forEach; de lo contrario será ignorado.

Así es como funcionan los métodos de Predicate: prueban hipótesis y le devuelven si son verdaderas o falsas.

Si ejecutamos este código, obtenemos este resultado:

Ejemplo

Simplemente imprimió los números pares, como se esperaba.

Y con respecto al BiPredicate, obtuvimos el mismo comportamiento, la única diferencia es que vamos a recibir dos parámetros para verificar en lugar de uno. Compruébelo a continuación:

Ejemplo:

Ejemplo

Aquí tenemos un BiPredicate que recibe dos parámetros, palabra y tamaño, y verifica si tienen el mismo valor.

En la primera prueba, vamos a recibir True, y en la segunda, vamos a recibir False.

Función y BiFunción

La interfaz funcional Function es la más genérica. Tiene la definición más básica de función: recibe algo y devuelve algo.

Echemos un vistazo a la clase:

Ejemplo

Comencemos con Function. Aquí podemos ver que el método aplicado recibe una variable genérica (recuerde que genérico significa que podría ser de cualquier tipo) y devolverá otra variable genérica.

Una cosa importante aquí es que incluso si las palabras genéricas son diferentes, T y R, no significa que el método apply() no pueda recibir y devolver el mismo tipo.

Entonces, vayamos al código. Una vez más, usaremos la API Stream para este ejemplo.

Aquí vamos a utilizar el método .map(), que necesita una Función para ser ejecutada.

Así que, en este código:

Ejemplo

Estamos obteniendo el número, que es un valor Integer, y devolviendo un Double. Por lo tanto, estamos pasando un argumento al mapa y recibiendo una respuesta.

*En este caso, estamos dando un valor Integer y recibiendo como respuesta un valor Double.

Pero, por ejemplo, podríamos hacer esto:

Ejemplo

Es un valor entero como argumento y su retorno es un valor entero también. La función lo permite. En cuanto a BiFunction, tiene el mismo comportamiento, excepto que recibe dos argumentos al igual que BiPredicate.

Ejemplo

Comprobémoslo en los siguientes ejemplos:

Ejemplo

En el primer ejemplo (sumaNúmeros), tenemos una BiFunción que recibe dos argumentos -de tipo Entero- y devuelve también un tipo Entero. Estamos haciendo una suma y cuando usamos el apply() da como resultado el número 3 (1 + 2).

A continuación, tenemos otro ejemplo, pero, esta vez, devuelve un valor Double. Tenemos 2 números Integer y, tras llamar a Math.pow(), nos devuelve un valor Double. Una vez que ejecutamos el método apply(), obtendremos el resultado: 4.0 (Double)

En resumen: damos uno o dos argumentos y recibimos algo a cambio. El significado básico de una función.

UnaryOperator y BinaryOperator

Echemos un vistazo a su clase:

Ejemplo

El UnaryOperator extiende una función, pero en su constructor se define el mismo tipo de argumentos, ¿lo notó?

We have UnaryOperator <T> extending to Function <T, T>.

Está definiendo el tipo permitido para esta interfaz, y como todos estos genéricos son la misma letra (T), significa que solo podemos trabajar con el mismo tipo de variables; nuestros argumentos y retornos deben ser del mismo tipo para funcionar en el UnaryOperator.

UnaryOperator tiene el mismo comportamiento que Function, pero solo funciona con los mismos tipos.

Por ejemplo:

Ejemplo

Basta con definir un tipo en su constructor, y se extenderá a la Función, y como podemos ver en el código anterior, sólo podemos trabajar con variables del mismo tipo.

Es lo mismo que los demás.

Se extiende a BiFunction. Entonces, vamos a recibir dos argumentos y devolver uno, todos con el mismo tipo.

Veamos esto:

Ejemplo

Usamos Stream API nuevamente.

Ahora, vamos a utilizar el método reduce(). Recibirá dos argumentos y devolverá un valor del mismo tipo.

En nuestro caso, obtuvimos una matriz de números, 1 a 5, y vamos a sumar todos los valores.

*Como reduce devuelve un valor opcional, usaremos .ifPresent() para imprimir nuestro resultado.

Entonces, como resultado aquí, tendremos: 15 (1 + 2 + 3 + 4 + 5)

*Se repetirá hasta que toda la matriz pase a través de él.

Para terminar, veamos un ejemplo de cómo funciona todo en conjunto:

  • Aquí estamos obteniendo sólo los números pares;
  • Luego, transformarlos en dobles;
  • Luego resumiéndolos a todos;
  • Y si hay un número al final, lo imprimimos en la consola.

Solo una última cosa: las interfaces funcionales también aceptan la referencia de método y el código podría ser así:

Ejemplo

¡Eso es todo! ¡Espero que pueda ayudarlo a crear un código mejor y más limpio! Además de ayudarlo a comprender cómo funcionan las funciones.

Inicie su transformación con nosotros

Sensedia está especializada en soluciones de arquitectura basada en eventos, con experiencia desde la creación de estrategias hasta su implementación.

Su arquitectura digital es más integrada, ágil y escalable.

Acelere la entrega de sus iniciativas digitales a través de APIs, Microservicios e Integraciones menos complejas y más eficientes que impulsen su negocio.