La programación funcional en java es más que lambdas, es un nuevo estilo de programación que se enfoca en que vas a resolver y no en como resolverlo.
Java que es conocido por su fuerza en la programación orientado a objetos y desde su versión 8 incorpora la posibilidad de agregar programación funcional.
Para entender que es la programación funcional debemos entender primero que es una función.
Tradicionalmente en programación una función(método) define el comportamiento de un objeto por medio de una serie de instrucciones . En programación funcional una función es también un tipo de dato lo que ahora permite en programación funcional se pueda pasar como parámetro y retornar a otras funciones.
Veamos el siguiente ejemplo:
En este ejemplo podemos ver que dentro de la función functionExample tenemos declarada una función de nombre function de tipo Funtion. Dentro de ella tenemos el método apply que recibe un String y retorna el mismo String con “!!!” concatenado y después retorna toda la función de la misma manera que lo haríamos con cualquier otro tipo de dato(String, int, etc).
Ahora vamos veamos el siguiente código:
En este código estamos invocando al método functionExample y almacenando su valor de retorno (en este caso otra función) en funcionExample de tipo Function para después invocar en el método apply e imprimir la salida Hello !!!
¿Qué es una lambda?
Seguramente cuando piensas en programación funcional en lo primero que piensas es en las Lambdas ¿Pero que son las lambdas? En términos sencillos una Lambda es una función la cual no tiene un nombre(función anónima),
Ejemplo con sintaxis de lambda:
En este ejemplo estamos retornando un Tipo de dato Function que no tiene un nombre asignado y puede usarse de la misma manera que en el primer ejemplo. La salida en este caso seria 10.
Explicación de la sintaxis lambda.
Las lambdas tienen la siguiente sintaxis dependiendo de los parámetros de entrada, operaciones y retorno.
Un parámetro de entrada y retorno
x -> x.length();
Dos parámetros de entrada y retorno
(x,y) -> x * y;
Mas de una operación
(x, y) -> {
System.out.println(x + “ + “ + y);
return x + y;
};
Sin parámetros ni retorno
() -> {}
¿Cuándo ocupar una lambda?
- Cuando el comportamiento de uso es único
- En una regla que solo se requiere en un lugar
- Cuando es una función extremadamente simple
¿Qué es un Predicate en Programación funncional en Java?
Predicate es un tipo de función que trabaja sobre un tipo y devuelve un boolean. Su función principal es comprobar si una condición es valida o no.
Veamos el siguiente ejemplo:
Creamos un simple Pojo con esta estructura.
Vamos a evaluar si el estudiante aprobó o no la materia utilizando un predicado.
En el fragmento de código anterior creamos un objeto de tipo Student y le asignamos el nombre de “Juan” con la calificación de 8. También tenemos un predicado que recibe un objeto de tipo Student y compara el parámetro score de este objeto Student verificando que sea mayor o igual a 6. Por ultimo invocamos el método test que nos ayudara a hacer la condición declarada en nuestro predicado dicho método recibe el objeto a comparar.
Salida: “Student approved: true”
¿Qué son los Consumers?
Un Consumer es una expresión que acepta un solo valor y no devuelve ninguno. Su método mas importante es accept. Un uso de Consumer es realizar operaciones sobre un tipo de datos por ejemplo de un listado guardar su contenido en una base de datos.
En este ejemplo creamos un Consumer(StudentConsumer) y un objeto Student que será el que reciba StudentConsumer y en este caso solo para ejemplificar la operación que se realizara sobre el objeto consumido es imprimir su contenido en consola, esto en un ambiente profesional podría ser por ejemplo un que ese objeto Student se envié a guardar a una base de datos al mandar llamar al método accept.
¿Qué es un Supplier?
Un Supplier es una expresión que no tiene parámetros pero devuelve un resultado. Su método principal es get. Se encarga de proveer datos.
El el siguiente código podemos ver un ejemplo de un Supplier de tipo Student, el cual no recibe ninguno parámetro y la operación a ejecutar es la creación de un objeto nuevo que se retorna al ejecutar el metodo get.
¿Para que sirve una Functional interface?
Java nos provee de interfaces con las cueles podemos escribir nuestras propias funciones como pueden ser Predicate, Consumer, Supplier, etc. Pero también podemos definir nuestra propia estructura de funciones ocupando las interfaces funcionales. Una interfaz funcional utiliza la anotación @FunctionalInterface y puede tener solo un método abstracto es decir que no tiene código implementado.
Veamos el siguiente ejemplo:
Supongamos que queremos definir una interfaz funcional para el calculo de calificaciones en una escuela. Para aprobar cada materia tienes que realizar dos exámenes pero cada examen tiene diferente valor dependiendo la materia.
Vamos declarar ahora nuestra interfaz funcional
Podemos ver que SAMInterface tiene solo un método abstracto llamado getScore que recibe dos parámetros aun sin definir(aunque si queremos ser mas estrictos podemos poner cualquier tipo definido como Internet por ejemplo) y tiene un parámetro P de retorno también sin definir.
Ahora vamos a darle una funcionalidad a la interfaz declarada anteriormente. Creamos la variable finalScoreMath y ahora si declaramos el tipo de dato de entrada y de retorno en este caso double.
El calculo de la calificación para matemáticas será que el primer examen valga el 40% y el segundo examen 60%. Mandamos a imprimir la calificación llamando ejecutar el método getScore pasando la calificación de tipo double en los parámetros de entrada. El mismo proceso sigue la materia de Ingles solo que con diferentes porcentajes de valor para cada examen y para cada calificación.
¿Qué son los Métodos default?
A partir de Java 8 podemos crear interfaces funcionales que solo pueden tener un método abstracto pero pueden tener varios métodos definidos mediante métodos default.
Para demostrar su uso continuemos con el ejemplo anterior. Supongamos que para pasar cada materia se tiene lograr una calificación mayor o igual a 6. En cada implementación de código de cada materia tendríamos que codificar la comparación del valor obtenido de getScore contra el valor 6 para determinar si se aprobó o no la materia. Pero con un método default podríamos simplificar todo esto.
Modifiquemos un poco el método anterior.
Ahora getScore recibe y retorna valores de tipo Double. Agregamos el método por default isApproved que evalúa el valor obtenido getScore contra 6 y regresa true o false dependiendo de la calificación de los exámenes y de el porcentaje de valor de cada examen definido en la implementación del código como se muestra a continuación .
¿Qué es la referencia a métodos en programación funcional en Java?
Una lambda sigue siendo una función es decir un método por lo tanto en programación funcional podemos sustituir una función lambda con un método tradicional por medio de la referencia a métodos usando el operador :: (Dos veces dos puntos).
Para demostrarlo seguiremos utilizando el ejemplo anterior.
En el ejemplo anterior tenemos la Interfaz DefaultMethodDemo con el método abstracto getScore el cual definíamos con la lampda (s1,s2) -> s1*.4 + s2*.6 . Ahora haremos algo equivalente utilizando un método que se encuentra en otra clase.
En esta clase estamos creando una lógica similar a la de la lambda con dos parámetros de entrada como las calificaciones de los exámenes y el promedio de ambas como calificación de salida.
En el código anterior podemos ver la creación del objeto MethodReferenceDemo y en la siguiente línea vemos el uso de la referencia al método ya existente congratulationMessage . Esto sirve al igual que en el ejemplo en que se usan lambdas para crear la implementación al método getScore de la interfaz funcional DefaultMethodDemo.
¿Para que sirve la clase Optional?
Optional es una nueva herramienta en programación funcional en Java que nos ayuda a lidiar con el famoso NullpointerException. Optional nos proporciona otra manera de lidiar con este error evitándonos cachar las excepciones (lo cual ahorra costo en rendimiento) y en lugar de eso controlar la ausencia de valor permitiéndonos si así fuera el caso elegir un valor alternativo o simplemente realizar otra acción.
Supongamos que tenemos un método que nos devuelve el segundo nombre de un alumno. Para solamente del fines del ejemplo devolverá un null.
Ahora queremos saber cuantos caracteres componen ese segundo nombre, tendríamos algo similar a esto.
Lo cual nos devolvería un NullPointerExcepcion. Vemos como se vería utilizando optional.
En el segundo método podemos ver que estamos declarando que nuestro valor de retorno es un Optional<String> y declaramos un Optional y a su método ofNullable le enviamos el valor de getSecondName que solamente regresa un null.
Para obtener el valor invocamos al método y almacenamos su valor de retorno en un Optional de tipo String y validamos si hay un valor en el, si lo hay imprimimos el numero de caracteres del String obteniéndolo con el método get. Hasta ahora esto realmente no es muy diferente a hacer una validación comparándolo con diferente de null. Pero Optional nos da muchas otras funcionalidades. Por ejemplo obtener un valor alternativo.
En el caso anterior se usa el valor “Paco” en caso de que la variable secondName de tipo optional sea null.
En este ejemplo en caso de encontrar null se mandara a llamar el método getFirstName para obtener otro valor.
La clase optional es muy amplia y esta fue solo una pequeña muestra de en que casos utilizarla.
¿Cómo recorrer colecciones con ForEach en Java con programación funcional?
A partir de Java 8 las colecciones agregan el métodos que junto con el uso de lambdas nos hará mas sencillo el uso de las listas. Vemos solo algunos estos métodos.
En el código anterior podemos ver el método forEach el cual recibe un lambda el cual imprime los valores del arreglo.
Podemos remover elementos de una lista usando el método removeIf y pasándole un predicate(Que ya vimos anteriormente).
Ahora solo faltaría mandar llamar a los métodos descritos arriba
Programación funcional y Streams en Java
La clase Stream es una especie de lista que puede tener elementos y se puede iterar la diferencia entre colecciones y Stream es que Stream es autoiterable. Imaginemos a Stream como un flujo de datos moviéndose sin esperar que alguien los mueva.
Podemos generar Streams de muchas fuentes por ejemplo
Otro ejemplo
También podemos volver rápidamente a un Objeto de tipo colección:
Un punto interesante es podemos paralelizar el Stream sin muchas complicaciones.
Un punto importante a considerar en Java con la programación funcional es que un Stream solo puede utilizarse una vez. Si Queremos utilizarlo una segunda vez tendremos como respuesta una excepción. Para resolver esto muchas funciones regresan un segundo Stream derivado del primero.
Imprimir el contenido de un Stream.
Podemos ver que al igual que Collections, Stream cuenta con el método forEach para recorrerlo. Si quisiéramos volver a operar stringNumbers no nos regresaría un error dado que dicho Stream ya fue utilizado.
Transformar contenido usando el método Stream Map
Lo que hace el método map es operar sobre nuestro Stream y devolvernos y nuevo Stream ya sea con la misma información o modificada de alguna forma.
En el ejemplo anterior nuestro stream stringNumbers contiene números pero de tipo String ahora vamos a convertirlo a Integer a multiplicarlo por 10 y a imprimir el resultado.
En el ejemplo podemos observar como el método map recibe la Lambda que transforma el Stream y retorna un nuevo Stream (lo que resuelve el problema de poder solo usar una ves un Stream) ahora de tipo Integer que el que después recorremos.
¿Cómo utilizar Stream Filter?
Filter es un metodo que como su nombre lo dice nos ayuda a filtrar información del Stream. Vemos el siguiente ejemplo.
En este ejemplo estamos transformando con map el Stream de Strings a Stream de Integers y multiplicándolo como en el ejemplo anterior pero ademas estamos filtrando el nuevo Stream generado con el método filter pasandole el predicado que valida que el valor de cada elemento del Stream sea mayor o igual que 60.
¿Para que sirve FlatMap?
Flatmap es la combinación de las operaciones Map y Flat lo que significa que primero hace una operación a sus elementos y luego la aplana. Para entenderlo mejor vamos al siguiente ejemplo .
Supongamos que tenemos dos grupos, cada grupo con distintos alumnos.
En el código anterior llenamos una lista de grupos con dos elementos y cada grupo con tres alumnos y al final convertimos la lista de grupos en un Stream y lo retornamos.
Ahora supongamos que queremos felicitar a todos los alumnos de esos dos grupos. Para ello utilizaremos flatMap para aplanar la lista de estudiantes y después recorreremos el Stream completo aplanado para felicitar a cada alumno.
Lo que obtenemos por salida del ejemplo anterior es lo siguiente.
Juan Felicitaciones
Carlos Felicitaciones
Luis Felicitaciones
Santiago Felicitaciones
Felipe Felicitaciones
Leo Felicitaciones
Pon a prueba tus conocimientos
Conclusión
La programación funcional ahora es posible en Java. Ahora conocemos los conceptos de Function, Predicate, Consumer y Supplier. Conocemos la sintaxis de una lambda, sabemos como utilizar las nuevas funcionalidades del forEach así como los Streams y los métodos que existen para operar en ellos.
Recuerda que la programación funcional se trata de ocuparnos mas en que se va a realizar mas que en como realizarlo.
Esto es solo un poco de lo mucho que puedes hacer con Java. Java es un uno de los lenguajes mas utilizados profesionalmente y muy bien pagado. Si te gusta el backend Java es sin duda la mejor opción para especializarte.
Te recomiendo hondar en el tema. Puedes buscar en la documentación oficial o elegir por un curso que te lleve de la mano.
Yo te puedo recomendar el siguiente que es muy completo y tiene mas de 52,000 estudiantes en udemy.