Skip to content

2 Expresiones lambda

Introducción

Con objeto de incorporar a Java funcionalidades propias de la programación funcional, Java 8 trajo consigo dos nuevas sintaxis para representar interfaces funcionales: expresiones lambda (lambda expressions) y referencias a método (method references).

Una expresión lambda es una nueva sintaxis con la que representar la implementación del método abstracto de interfaces funcionales indicando además la lista de parámetros con sus tipos y el tipo de retorno. De esta manera, podemos escribir el código de apartados anteriores mediante una expresión lambda, haciéndolo mucho más legible.

El nuevo operador para las expresiones lambda se denomina operador lambda y tiene la forma de flecha ->. Divide la expresión lambda en dos partes: la parte izquierda especifica los parámetros necesarios y la parte derecha contiene el cuerpo de la expresión. Este cuerpo puede estar compuesto por una única expresión o puede ser un bloque de código. Cuando es una única expresión se denomina lambda de expresión y cuando es un bloque de código se denomina lambda de bloque.

Debemos tener en cuenta que cuando se especifica una expresión lambda, no indicamos nada sobre la interfaz funcional a la queremos aplicarla, es decir, dependiendo de donde se esté usando la expresión lambda, el compilador deberá determinar si la firma de la expresión lambda coincide con la firma del método abstracto de la correspondiente interfaz funcional. Si la expresión lambda no incluye los tipos de los parámetros, el compilador tratará de inferirlos a partir de los tipos de los parámetros del método abstracto de la interfaz funcional.

El ejemplo anterior de la interfaz BinaryOperator<Integer>, si lo realizamos con una expresión lambda, resultaría de la siguiente manera:

public class BinaryOperatorWithLambda{
    public void show(){

        ShowBinaryOperator binOper = new ShowBinaryOperator();

        System.out.printf("12 + 6 = %d\n", binOper.calculate(12, 6, (t,u) -> t + u));
        System.out.printf("12 - 6 = %d\n", binOper.calculate(12, 6, (t,u) -> t - u));
        System.out.printf("12 / 6 = %d\n", binOper.calculate(12, 6, (t,u) -> t / u));
        System.out.printf("12 * 6 = %d\n", binOper.calculate(12, 6, (t,u) -> t * u));
    }

    public static void main(String[] args){
        new BinaryOperatorWithLambda().show();
    }
}

Veamos el ejemplo de ordenación de la lista hecho de las dos maneras, con una clase inline anónima y con una expresión lambda:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class LambdaExpression{

    public void show(){
        List<Integer> list = Arrays.asList(3, 2, 6, 1, 5, 4);
        list.sort(new Comparator<Integer>(){
            @Override
            public int compare(Integer o1, Integer o2){
                return Integer.compare(o1, o2);
            }
        });

        for(Integer i : list){
            System.out.printf(" %d ", i);
        }
        System.out.println();

        list.sort((o1, o2) -> Integer.compare(o1, o2)); 

        for(Integer i : list){
                System.out.printf(" %d ", i);
        }
    }

    public static void main(String[] args){
        new LambdaExpression().show();
    }
}

Aún así, debemos tener en cuenta que un objeto de una clase inline anónima y una expresión lambda no son lo mismo, porque una expresión lambda no crea ninguna clase adicional y además en una expresión lambda no se puede almacenar ningún estado, mientras que en una clase anónima inline sí. Un objeto de una clase anónima inline genera un archivo de clase independiente durante la compilación que aumenta el tamaño del archivo jar. Sin embargo, una expresión lambda se convierte en un simple método privado. En una lambda, this representa la clase actual desde la que se está usando la lambda. En el caso de una clase anónima, this representa ese objeto de la clase anónima en particular.

Como vemos, las expresiones lambda son muy útiles para simplificar el código, pero presentan un problema: para que podamos usar esta nueva sintaxis de expresión lambda es necesario que la interfaz contra la que la usemos sea una interfaz funcional, es decir, que sólo contenga un único método abstracto, porque si tuviera por ejemplo dos métodos, ¿el código proporcionado mediante la expresión lambda cuándo se debería ejecutar, cuando se ejecute uno o cuando se ejecute el otro? Por este motivo las expresiones lambda solo se pueden usar con interfaces funcionales.

Se puede almacenar una expresión lambda en una variable cuyo tipo corresponda a una interfaz funcional compatible con dicha lambda, es decir, cuya firma del método abstracto de la interfaz sea compatible con la expresión lambda. Por ejemplo:

Comparator<Integer> comparador = (o1, o2) -> Integer.compare(o1, o2);
BinaryOperator<Integer> operacionBinaria = (o1, o2) -> o1 + o2;

Sintaxis de la expresión lambda

Veamos la sintaxis de la expresión lambda:

(tipo param1, tipo param2, ...) -> {
    // instrucciones
    return valorRetorno;
}

Podemos omitir el tipo de dato de cada parámetro siempre y cuando el compilador pueda inferirlos (deducirlos) a partir del context, es decir, a partir de los tipos de los parámetros del método abstracto de la interfaz funcional para la que se está usando.

El cuerpo de las expresiones lambda puede contener los mismos tipos de sentencias que cualquier otra función, como sentencias condicionales, iterativas o try catch.

Si se específica un único parámetro y no se específica el tipo de éste sino que es inferido, podemos omitir los paréntesis. Por ejemplo:

x -> x + x;

Si no se específica ningún parámetro, es obligatorio poner los paréntesis. Por ejemplo:

() -> System.out.println("Hola mundo");

En el cuerpo podemos omitir las las llaves si éste contiene una única expresión o una única sentencia que no retorna valor. Si el cuerpo contiene una única expresión, ésta será evaluada y la expresión lambda retornará el valor obtenido.

Si el cuerpo contiene más de una sentencia y la expresión lambda debe retornar un valor, entonces debemos usar una sentencia return valor.

Si el cuerpo contiene una sentencia return valor, forzosamente debemos poner las llaves, incluso si el cuerpo contiene una única sentencia, ya que returnno es un expresión.

Un expresión lambda se puede usar como argumento de un parámetro de tipo interfaz funcional y como valor de retorno de una función cuyo tipo de retorno sea una interfaz funcional. sin embargo habrá ocasiones donde debamos realizar un cast explícitamente para indicar la interfaz funcional a la que queremos aplicar una determinada expresión lambda.

Ámbito de una expresión lambda

Una expresión lambda puede acceder a las variables static definidas en el ámbito en el que la expresión lambda es usada. También puede acceder a las variables locales pero que sean eficazmente finales, es decir, variables cuyo valor no cambia una vez asignado. Estas variables no tienen necesariamente que estar definidas como final. Una expresión lambda también tiene acceso a this, lo que hace referencia a la instancia de invocación de la clase contenedora de la expresión lambda.

Si en el cuerpo de una expresión lambda con más de una sentencia definimos una variable local, debemos tener en cuenta que dicha variable tendrá como ámbito el correspondiente a donde se ha definido la expresión lambda, ya que la expresión lambda no define su propio ámbito independiente. Si ya existiera una variable con el mismo nombre en dicho ámbito se produciría un error de compilación. Por ejemplo:

int z = 2;
BinaryOperator<Integer> operacion = (x, y) -> {
    int z = 4; //(1)!
    System.out.println(x + z);
}
  1. ¡ERROR! z ya está definida en el ámbito

Una expresión lambda puede generar una excepción. No obstante, si genera una excepción comprobada, esta tendrá que ser compatible con la excepción (o excepciones) indicadas en la cláusula throws del método abstracto de la interfaz funcional. Veamos un ejemplo:

public interface FunctionalInterface{
    int ioAction() throws Exception;
}
public class LambdaException{
    public void show(){
        FunctionalInterface fi = () -> {
            Scanner keyboard = new Scanner(System.in);
            int num = keyboard.nextInt();
            return num;
        };


        try{
            System.out.printf("Introduce un número: ");
            System.out.println(method(fi));
        } catch (Exception e){
            System.out.println("Error en la lectura");
        }
    }

    public int method(FunctionalInterface fi) throws Exception{
        return fi.ioAction();
    }

    public static void main(String[] args){
        new LambdaException().show();
    }
}

Limitaciones de las expresiones lambda

El código de una expresión lambda se convierte en el código del método abstracto de la interfaz funcional que implementa. or tanto, las expresiones lambda no sirven para sobrescribir la implementación por defecto de un método default de la interfaz, sino que debe tratarse de un método abstracto. De hecho si la interfaz solo tiene un método default, no será considerada una interfaz funcional. Si nos vemos en la obligación de sobrescribir un método default de una interfaz, entonces tendremos que usar una clase anónima inline.

Por otro lado, una expresión j*lambda* no es consciente de qué interfaz funcional concreta está implementando, por lo que no puede llamar a su vez a métodos privados ni default de la interfaz.

Finalmente, las expresiones lambda no pueden usarse con clases abstractas que tengan un único método abstracto, solo se pueden usar con interfaces funcionales.

Referencias a método

Una referencia de método (method reference) es una abreviación de la lambda y se da siempre y cuando es una expresión de una única línea y los parámetros que se le pasa a la lambda son utilizados en la expresión.

Una referencia de método permite hacer referencia a un método sin ejecutarlo. Al evaluar una referencia de método, también se crea una instancia de una interfaz funcional.

  • Sintaxis para métodos estáticos: NombreClase::nombreMétodo
    • v -> Math.sqrt(v) equivaldría a Math::sqrt
    • (o1, o2) -> Integer.compare(o1, o2) equivaldría a Integer::compare
  • Sintaxis para métodos de instancia: refObj::nombreMétodo
    • persona -> persona.getNombre() equivaldría a Persona::getNombre
    • n -> System.out.println(n) equivaldría System.out::println
    • (cadena1, cadena2) -> cadena1.compareToIgnoreCase(cadena2) equivaldría a String::compareToIgnoreCase: en este caso, el primer parámetro de la expresión lambda es quien ejecuta el método y el resto de parámetros se pasan como argumentos en la llamada.
    • empleado -> jefe.comparaSalarioCon(empleado) equivaldría a jefe::comparaSalarioCon: en este caso, un objeto ajeno a la expresión lambda es quien ejecuta el método y dicho método recibe como argumento el (o los) parámetro(s) de la expresión lambda.
    • () -> new TreeMap<>() equivaldría a TreeMap::new: este tipo se conoce como referencia a constructor, que emplearemos cuando queramos que se llame al método constructor de una clase.
    • i -> new int[i] equivaldría a int[]::new: en este caso, lo que queremos es que se llame al constructor de un array.
  • Sintaxis para métodos genéricos:
    • Estáticos: NombreClase::<T>nombreMétodo
    • Métodos de instancia: refObj::<T>nombreMétodo