Expresiones lambda
Las expresiones lambda no son complejas de usar y entre otras ventajas permiten pasar comportamiento como valor, algo muy utilizado en Javascript, y abreviar líneas de código sin perder legibilidad en el código. Es por esto que Java las adoptó definitivamente en la JavaSE 8. Pero, para que las expresiones lambda sean verdaderamente útiles, hay que llegar a entenderlas. Hasta que eso ocurre pueden llegar a ser bastante confusas.
Si nos atenemos a la definición oficial de expresión lambda diríamos que son funciones anónimas que implementan una interfaz funcional, que así dicho suena muy técnico, pero si te lo dicen así sin más, puede dejarte algo frío. A mi es lo que me pasó, así que me puse manos a la obra para intentar aterrizar el concepto y desgranar la definición teórica haciéndola comprensible para mortales como yo. De ese germen surgió esta entrada.
Lo que me quedó claro de la definición es qué, para entender las expresiones lambda hay que entender qué es una función anónima y que es una interfaz funcional.
Función anónima
Las funciones anónimas permiten crear instancias de un objeto que implementa un interfaz en particular sin que para ello sea necesario desarrollar la clase que implementa esa interfaz explícitamente.
¡Toma ya!
Esto se ve claro con un ejemplo. Imaginemos una interfaz que ofrece métodos para una calculadora:
package com.tecnicomio.pruebas.funcionesanonimas;
public interface ICalculadora {
Integer suma (Integer operando1, Integer operando2);
}
De manera tradicional tendríamos una clase que implementara la interfaz:
package com.tecnicomio.pruebas.funcionesanonimas;
public class Calculadora implements ICalculadora {
@Override
public Integer suma(Integer operando1, Integer operando2) {
return operando1 + operando2;
}
}
Posiblemente, utilizaríamos esta clase en otra parte del código de la siguiente manera:
[...]
ICalculadora calculadora = new Calculadora();
Integer operando1 = 10;
Integer operando2 = 22;
Integer resultado = calculadora.suma(operando1, operando2);
System.out.println(operando1 + "+" + operando2 + "=" + resultado);
[...]
Ahora haré una aproximación con funciones anónimas. La interfaz se mantiene, pero la clase desaparece.
El código resultado sería:
[...]
Integer operando1 = 10;
Integer operando2 = 22;
System.out.println(operando1 + "+" + operando2 + "=" +
new ICalculadora() {
@Override
public Integer suma(Integer operando1, Integer operando2) {
return operando1 + operando2;
}
}.suma(operando1, operando2));
[...]
Repasando qué hemos conseguido con la función anónima, llegamos a las siguientes conclusiones:
- Nos hemos ahorrado la definición y creación de la clase, que en el caso de clases que se usan en un único sitio, puede ser un gran ahorro. Así evitamos contaminar el proyecto con esas clases que no se reutilizan en otras partes.
- Cuando la implementación es corta, como en el caso de este ejemplo, en el que el código se encuentre directamente en el lugar donde se usa, puede hacer que éste sea más entendible (aunque tal vez menos legible).
- Permite seguir manejando correctamente las variables locales y miembros de la clase sin tener que definir un constructor para poder recibirlas y usarlas.
En este ejemplo, es probable que el ahorro de crear la clase, versus la disminución de legibilidad, haga que no merezca suficientemente la pena, pero pongamos un ejemplo donde verdaderamente las funciones anónimas dan la talla. Imaginemos una ventana con 15 botones y sus respectivos 15 event listeners cada vez que el usuario hace click en ellos. En este caso, habría que crear 15 clases donde cada una implementa el interfaz ActionListener. Clases que solo se usan en el punto del código donde se implementa el formulario. En este caso, usar funciones anónimas es mucho más rentable.
Interfaz Funcional
Un interfaz funcional es aquel interfaz que tiene únicamente un método y este método es abstracto, es decir un método sin implementar. Las interfaces funcionales fueron agregadas a partir de la versión JavaSE 8 y vienen de la mano de las expresiones lambda.
A continuación dos ejemplos de interfaz funcional. El primero es un ejemplo al uso, con un único método abstracto.
package com.tecnicomio.interfazfuncional;
public interface IInterfazFuncionalSimple {
public String saludo(String nombre);
}
El segundo es un ejemplo un poco más rebuscado pero igualmente válido; un único método abstracto y varios métodos default.
package com.tecnicomio.interfazfuncional;
public interface IInterfazFuncionalRebuscada {
public String saludo(String nombre);
public default String holaMundo() {
Return "Hola mundo.";
}
}
Para asegurarnos que la interfaz cumple con las reglas de las interfaces funcionales podríamos anotar la interfaz con @FunctionalInterface. En este caso si introdujéramos más de un método abstracto (sin implementación) el compilador nos daría el error: «Multiple non overriding abstract methods found in interface com.tecnicomio.pruebas.interfazfuncional.InterfazFuncionalAnotada». En caso de no introducir ningún método abstracto, nos daría el error: «No Target method found».
package com.tecnicomio.interfazfuncional;
@FunctionalInterface
public interface IInterfazFuncionalAnotada {
public String saludo(String nombre);
public default String holaMundo() {
Return "Hola mundo.";
}
}
En el ejemplo de la calculadora, la interfaz ICalculadora es un claro ejemplo de Interfaz Funcional, sin anotar.
Expresiones lambda
Y ahora que ya han quedado un poco más claros los conceptos «funciones anónimas» e «interfaces funcionales» volvamos a la definición de expresión lambda: las expresiones lambda son funciones anónimas que implementan una interfaz funcional.
Por tanto, podríamos decir que son una evolución de las funciones anónimas que pretenden simplificar aún más el código, pero que para conseguir esta simplificación, obligan a que la funcionalidad de la expresión lambda implemente una interfaz funcional.
Sintaxis de las expresiones lambda
La sintaxis de las expresiones lambda es:
(Parametros) -> { cuerpo expresión lambda }
Teniendo en cuenta qué:
- El operador lambda (->) separa la declaración de parámetros del cuerpo de la función.
- Parámetros
- Cuando se tiene un solo parámetro pueden omitirse los paréntesis.
- Cuando no se tienen parámetros, o cuando se tienen dos o más, sí es necesario su uso.
- Cuerpo de la expresión lambda
- Cuando el cuerpo de la expresión lambda tiene una única línea pueden omitirse las llaves y no se necesita especificar la cláusula return en el caso de que se devuelva valor.
Ejemplos de expresiones lambda pueden ser:
- Expresión lambda con un único parámetro y una única línea: num -> num+10
- Expresión lambda sin parámetros y una única línea: () -> System.out.println(«Hola mundo»)
- Expresión lambda con dos parámetros y una única línea: (int operando1, int operando2) -> operando1 * operando2
- Expresión lambda con múltiples parámetros y varias líneas de función: (String nombre) -> {String retorno=»Hola «; retorno=retorno.concat(nombre); return retorno;}
La calculadora con expresiones lambda
Y como lo mejor para entender algo es verlo con un ejemplo, apliquemos las expresiones lambda al ejemplo de la calculadora que podría quedar así:
ICalculadora calculadora = (Integer operando1, Integer operando2)->(operando1+operando2);
Integer operando1 = 10;
Integer operando2 = 22;
System.out.println(operando1 + "+" + operando2 + "=" + calculadora.suma(operando1,operando2));
O si queremos ahorrar aún más líneas de código sin perder legibilidad podría quedar de esta otra forma:
System.out.println("En una línea:10+22=" + ((ICalculadora)(Integer operando1, Integer operando2)->(operando1+operando2)).suma(10,22));
Conclusiones
Como se puede ver en el ejemplo de la calculadora utilizar expresiones lambda tienen claros y algunos obscuros.
Para mí los claros son:
- Al hacer uso de funciones anónimas sin necesidad de crear clases anónimas se crea código más claro y conciso.
- Acercan Java a la programación funcional muy utilizado en lenguajes de script, como Javascript, donde las funciones juegan un papel protagonista. Esto permite poder pasar funciones como valores de variables, valores de retorno o parámetros de otras funciones, es decir, gracias a las expresiones lambda se puede pasar comportamiento como valor.
- Al usarlas en combinación con la API Stream se pueden realizar operaciones de tipo filtro/mapeo sobre colecciones de datos de forma secuencial o paralela siendo la implementación transparente al desarrollador.
- Logran un código más compacto y fácil de leer.
- Reducen la escritura de código.
El oscuro para mí es claro:
- Hay que entenderlas para sacarles el máximo partido. Y entenderlas es cambiar la manera de pensar del javero de toda la vida.