Una expresión lambda equivale a escribir una especie de función matemática del tipo
f(x) = x * 2 + 3
En este caso leemos que dado un x, el resultado será el de multiplicar por 2 el valor de x y sumarle 3.
Para expresar esto mismo con una expresión lambda hariamos:
(x) -> x * 2 + 3
Expresar esto en programación requiría escribir un método o función, que a su vez debe encapsulsarse en una clase.
public class Contenedora implements Funcion {
int funcion(int x) {
return 2 * x + 3;
}
}
Si la clase contenedora implementa una interfaz Funcion definida de esta manera
public interface Funcion {
int funcion(int x);
}
Entonces podemos asignar una referencia a un objeto de tipo Contenedora a una variable de tipo Funcion
y utilizar la variable para invocar al objeto que realiza la operación.
Funcion f1 = new Contenedora(); f1.funcion(25); // 25 * 2 + 3
Todo este envoltorio en clases es necesario debido a que anteriormente java no permitia asignar funciones a variables, solamente se podían asignar valores de tipos de datos primitivos y referencias a objetos. Las funciones eran elementos de segunda categoría y se tenían que encapsular en clases contenedoras.
A partir de Java 8 si una interfaz como la que hemos definido, Funcion, contiene un único método, se podrá asignar directamente a una variable del tipo de la interfaz una función cuya firma sea compatible con la firma de este método.
class Funciones {
static int dobleMas3(int x) {
return 2 * x + 3;
}
//mas funciones dentro de la clase
}
Podemos usar la funciín de arriba dobleMas3 de este modo:
Funcion f1 = Funciones::dobleMas3; f1.funcion(25); // 25 * 2 + 3
A la variable f1, de tipo Funcion (una interfaz de un único método) le asignamos directamente el nombre de una función.
Pero existe un detalle que todavía se puede mejorar: las clases anónimas permiten asignar a una variable del tipo de la interfaz una referencia a un objeto nuevo anónimo creado así porque no existe una clase entre las que implementan la interfaz que reuna las características que buscamos y solamente vamos a crear una instancia objeto sola esa vez. ¿Para qué crear una clase implementadora de la interfaz a propósito para luego sólamente utilizar una instancia de esa clase? Es el caso de los «listeners» de Swing
Bien, pues esta misma situación también se puede presentar cuando estamos considerando funciones. ¿Y si no existe en el código una función que realice las instrucciones que yo quiero llevar a cabo?¿La defino en alguna parte, le doy un nombre y luego la asigno como hemos visto en el ejemplo de la función dobleMas3?
La respuesta es que no es necesario, igual que existen los objetos anónimos existen las funciones anónimas, y estas son las denominadas expresiones lambda.
Por tanto, supongamos que queremos asignar a esa variable f1 una función que no existe, en este caso haríamos algo así:
Funcion f1 = (x) -> x * 2 + 3; //expresion lambda f1.funcion(25); // 25 * 2 + 3
Una vez visto el ejemplo, vayamos a la sintaxis. Una expresión lambda se compone de 2 partes: parte izquierda y parte derecha, separadas por los caracteres ->. En la izquierda es donde se indican las variables de la expresión y en la derecha está el calculo que se realiza con esos valores de entrada proporcionados.
Se han de cumplir una serie de requisitos para que una expresión lambda se puede asignar a una variable (o pasarse como argumento en una llamada)
- La expresión lambda debe definir en su parte izquierda tantos identificadores como el número de parámetros que tiene la función única definida en la interfaz de cuyo tipo es la variable o parámetro de la llamada al que queremos asignarle la expresión.
- La expresión que realiza el cálculo en la parte derecha, cuando se evalue, debe proporcionar un resultado cuyo tipo debe coincidir con el tipo de valor de retorno que define la función en la interfaz
Como consecuencia de todo esto, podemos concluir que a partir de este momento, comportamiento o funcionalidad acaban convirtiéndose en información almacenable en variables y es posible pasarlo a otras funciones que la esperan como argumento de llamada. De modo que, igual que el resultado de una función depende de los valores con los que es invocada, el comportamiento de la función puede cambiar si al llamar a la función pasamos como argumento distintos comportamientos (funcionalidad o código). La implementación de un método de una clase puede parametrizar no sólo información para llevar a cabo su tarea sino que además puede parametrizar comportamientos (fragmentos de código) en los cuales delegar. En otras palabras, una parte del código pueda porporcionar no solo datos sino también instrucciones o código a otra parte cuando esta última es invocada.