Expresiones Lambda en Java

Avanzado

A partir de JDK 8, se agregó una característica a Java que mejoró profundamente el poder expresivo del lenguaje. Esta característica es la expresión lambda (Lambda Expressions). Las expresiones lambda no solo agregaron nuevos elementos de sintaxis al lenguaje, sino que también simplificaron la forma en que se implementan ciertas construcciones comunes. De la misma manera que la adición de genéricos reformó Java hace años, las expresiones lambda continúan remodelando Java hoy en día. Ellos realmente son tan importantes.

La adición de expresiones lambda también proporcionó el catalizador para otras características de Java. Ya ha visto uno de ellos, el método default (Leer Métodos de Interfaces en Java), que le permite definir el comportamiento predeterminado para un método de interfaz.

Otro ejemplo son los métodos referenciados, que se describe más adelante, que le permite consultar un método sin ejecutarlo. Además, la inclusión de expresiones lambda dio como resultado la incorporación de nuevas capacidades en la biblioteca API.

Más allá de los beneficios que las expresiones lambda aportan al lenguaje, existe otra razón por la cual constituyen una parte tan importante de Java. En los últimos años, las expresiones lambda se han convertido en un foco principal del diseño de lenguaje de computadora. Por ejemplo, se han agregado a lenguajes como C# y C++. Su inclusión en Java lo ayuda a seguir siendo el lenguaje vibrante e innovador que los programadores esperan.

1. Introducción a las expresiones lambda

La clave para entender la expresión lambda son dos conceptos. El primero es la expresión lambda, en sí misma. El segundo es la interfaz funcional. Comencemos con una definición simple de cada uno.

  • Una expresión lambda es, esencialmente, un método anónimo (es decir, sin nombre). Sin embargo, este método no se ejecuta solo. En cambio, se usa para implementar un método definido por una interfaz funcional. Por lo tanto, una expresión lambda da como resultado una forma de clase anónima. Las expresiones lambda también se conocen comúnmente como cierres (closures).
  • Una interfaz funcional es una interfaz que contiene uno y solo un método abstracto. Normalmente, este método especifica el propósito previsto de la interfaz. Por lo tanto, una interfaz funcional típicamente representa una sola acción. Por ejemplo, la interfaz estándar Runnable es una interfaz funcional porque define solo un método: run(). Entonces, run() define la acción de Runnable. Además, una interfaz funcional define el tipo de objetivo de una expresión lambda. Aquí hay un punto clave: una expresión lambda solo se puede usar en un contexto en el que se especifica un tipo de objetivo. Otra cosa: una interfaz funcional a veces se conoce como tipo SAM, donde SAM significa Single Abstract Method.

Miremos más de cerca tanto a las expresiones lambda como a las interfaces funcionales.

Una interfaz funcional puede especificar cualquier método público definido por Object, como equals(), sin afectar su estado de “interfaz funcional”. Los métodos de objetos públicos se consideran miembros implícitos de una interfaz funcional porque se implementan automáticamente mediante una instancia de una interfaz funcional.

2. Fundamentos de la expresión Lambda

La expresión lambda se basa en un elemento de sintaxis y un operador que difieren de lo que ha visto en los temas anteriores. El operador, a veces denominado operador lambda u operador de flecha, es ->.

Divide una expresión lambda en dos partes:

  1. El lado izquierdo especifica los parámetros requeridos por la expresión lambda.
  2. En el lado derecho está el cuerpo lambda, que especifica las acciones de la expresión lambda.

Java define dos tipos de cuerpos lambda. Un tipo consiste en una sola expresión, y el otro tipo consiste en un bloque de código. Comenzaremos con lambdas que definen una sola expresión.

2.1. Lambdas que definen una sola expresión

En este punto, será útil mirar algunos ejemplos de expresiones lambda antes de continuar. Comencemos con lo que probablemente sea el tipo más simple de expresión lambda que puede escribir. Evalúa a un valor constante y se muestra aquí:

() -> 28.6

Esta expresión lambda no tiene parámetros, por lo que la lista de parámetros está vacía. Devuelve el valor constante 28.6. El tipo de devolución se infiere que es double. Por lo tanto, es similar al siguiente método:

double miMetodo() { return 28.6; }

Por supuesto, el método definido por una expresión lambda no tiene un nombre. Aquí se muestra una expresión lambda ligeramente más interesante:

() -> Math.random() * 100

Esta expresión lambda obtiene un valor pseudoaleatorio de Math.random(), lo multiplica por 100 y devuelve el resultado. Tampoco requiere un parámetro.

Cuando una expresión lambda requiere un parámetro, se especifica en la lista de parámetros en el lado izquierdo del operador lambda. Aquí hay un ejemplo simple:

(n) -> 1.0 / n

Esta expresión lambda devuelve el valor recíproco del parámetro n. Por lo tanto, si n es 4.0, el recíproco es 0.25. Aunque es posible especificar explícitamente el tipo de un parámetro, como n en este caso, a menudo no será necesario porque, en muchos casos, se puede inferir su tipo. Al igual que un método con nombre, una expresión lambda puede especificar tantos parámetros como sea necesario.

Cualquier tipo válido se puede usar como el tipo de devolución de una expresión lambda. Por ejemplo, esta expresión lambda devuelve true si el valor del parámetro n es par y false de lo contrario.

(n) -> (n % 2)==0

Por lo tanto, el tipo de retorno de esta expresión lambda es booleano.

Otro punto más antes de continuar. Cuando una expresión lambda tiene solo un parámetro, no es necesario rodear el nombre del parámetro con paréntesis cuando se especifica en el lado izquierdo del operador lambda. Por ejemplo, esta es también una forma válida de escribir la expresión lambda que se acaba de mostrar:

n -> (n % 2)==0

Sin embargo, por coherencia, este curso rodeará todas las listas de parámetros de expresión lambda con paréntesis, incluso aquellos que contienen solo un parámetro. Por supuesto, eres libre de adoptar un estilo diferente.

3. Interfaces funcionales

Como se ha dicho, una interfaz funcional es una interfaz que especifica sólo un método abstracto. Antes de continuar, recuerde que no todos los métodos de interfaz son abstractos. A partir de JDK 8, es posible que una interfaz tenga uno o más métodos default (predeterminados). Los métodos default no son abstractos. Tampoco lo son los métodos de interfaz estáticos o privados. Por lo tanto, un método de interfaz es abstracto sólo si no especifica una implementación. Esto significa que una interfaz funcional puede incluir métodos por defecto, estáticos o privados, pero en todos los casos debe tener un solo y único método abstracto. Debido a que los métodos de interfaz no predeterminados, no estáticos y no privados son implícitamente abstractos, no hay necesidad de usar el modificador abstract (aunque puede especificarlo, si lo desea).

3.1. Ejemplo de Interfaz funcional

Aquí hay un ejemplo de una interfaz funcional:

interface MiValor{
  double getValor();
}

En este caso, el método getValor() es implícitamente abstracto, y es el único método definido por MiValor. Por lo tanto, MiValor es una interfaz funcional, y su función está definida por getValor().

Como se mencionó anteriormente, una expresión lambda no se ejecuta por sí misma. Más bien, forma la implementación del método abstracto definido por la interfaz funcional que especifica su tipo de objetivo. Como resultado, una expresión lambda solo se puede especificar en un contexto en el que se define un tipo de objetivo. Uno de estos contextos se crea cuando una expresión lambda se asigna a una referencia de interfaz funcional. Otros contextos de tipo de objetivo incluyen inicialización de variable, declaraciones de return y argumentos de método, por nombrar algunos.

3.2. Contextos de tipo de objetivo

Vamos a trabajar a través de un simple ejemplo. Primero, una referencia a la interfaz funcional:

MiValor está declarado:

// Crea una referencia a una instancia de MiValor.
MiValor miVal;

A continuación, se asigna una expresión lambda a esa referencia de interfaz:

// Usa una lambda en un contexto de asignación.
miVal = () -> 28.6;

Esta expresión lambda es compatible con getValor() porque, al igual que getValor(), no tiene parámetros y devuelve un resultado double. En general, el tipo de método abstracto definido por la interfaz funcional y el tipo de expresión lambda debe ser compatible. Si no lo son, se producirá un error en tiempo de compilación.

Como probablemente pueda adivinar, los dos pasos que se muestran pueden combinarse en una sola declaración, si lo desea:

MiValor miVal = () -> 28.6;

Aquí, miVal se inicializa con la expresión lambda.

Cuando se produce una expresión lambda en un contexto de tipo de objetivo, se crea automáticamente una instancia de una clase que implementa la interfaz funcional, definiendo la expresión lambda el comportamiento del método abstracto declarado por la interfaz funcional. Cuando se llama a ese método a través del objetivo, se ejecuta la expresión lambda. Por lo tanto, una expresión lambda nos da una forma de transformar un segmento de código en un objeto.

En el ejemplo anterior, la expresión lambda se convierte en la implementación del método getValor(). Como resultado, lo siguiente muestra el valor 28.6:

// Llama a getValor(), que se implementa mediante la expresión lambda
// asignada previamente.
System.out.print1n("Un valor constante: " + miVal.getValor());

Debido a que la expresión lambda asignada a miVal devuelve el valor 28.6, ese es el valor obtenido cuando se llama a getValor().

3.3. Expresión lambda con uno o más parámetros

Si la expresión lambda toma uno o más parámetros, entonces el método abstracto en la interfaz funcional también debe tomar el mismo número de parámetros. Por ejemplo, aquí hay una interfaz funcional llamada MiValParam, que le permite pasar un valor a getValor():

interface MiValParam {
double getValor(double v);
}

Puede usar esta interfaz para implementar la lambda recíproca. Por ejemplo:

MiValParam miValor = (n) -> 1.0 / n;

Entonces puede usar miValor como:

System.out.println("El recíproco de 4.0 es: "+miValor.getValor(4.0));

Aquí, getValor() se implementa mediante la expresión lambda a la que se refiere miValor, que devuelve el recíproco del argumento. En este caso, 4.0 se pasa a getValor(), que devuelve 0.25.

Hay algo más de interés en el ejemplo anterior. Tenga en cuenta que el tipo de n no está especificado. Más bien, su tipo se deduce del contexto. En este caso, su tipo se deduce del tipo de parámetro de getValor() tal como lo define la interfaz MiValParam, es double. También es posible especificar explícitamente el tipo de un parámetro en una expresión lambda. Por ejemplo, esta es también una forma válida de escribir lo anterior:

(double n) -> 1.0 / n;

Aquí, n se especifica explícitamente como double. Por lo general, no es necesario especificar explícitamente el tipo.

Antes de continuar, es importante destacar un punto clave: para que una expresión lambda se use en un contexto de tipo de objetivo, el tipo de método abstracto y el tipo de expresión lambda deben ser compatibles. Por ejemplo, si el método abstracto especifica dos parámetros int, entonces la lambda debe especificar dos parámetros cuyo tipo sea explícitamente int o inferirse implícitamente como int por el contexto. En general, el tipo y el número de los parámetros de la expresión lambda deben ser compatibles con los parámetros del método y su tipo de retorno.

4. Expresiones Lambda en acción

4.1. Ejemplo 1

Con la discusión anterior en mente, veamos algunos ejemplos simples que ponen en práctica los conceptos básicos de expresión lambda. El primer ejemplo reúne las piezas que se muestran en la sección anterior en un programa completo con el que puede ejecutar y experimentar.

// Demostrar dos expresiones lambda simples.

// Una interfaz Funcional
interface MiValor {
    double getValor();
}

// Otra interfaz funcional
interface MiValParam {
    double getValor(double v);
}

class LambdaDemo{
    public static void main(String[] args) {
        MiValor miValor; // declarar una referencia de interfaz

        // Aquí, la expresión lambda es simplemente una expresión constante.
        // Cuando se asigna a miValor, se construye una instancia de clase
        // en la que la expresión lambda implementa el método getValor() en MiValor.
        miValor=()->28.6; //Una simple expresión lambda

        // Llama a getValor(), el cual es provisto por la expresión lambda previamente asignada.
        System.out.println("Un valor constante: "+miValor.getValor());

        // Ahora, se crea una expresión lambda parametrizada y se le asigna a una referencia MiValParam.
        // Esta expresión lambda devuelve lo recíproco de su argumento.
        MiValParam miValParam= (n)->1.0/n;

        // Llama a getValor(v) a través de la referencia miValParam.
        System.out.println("El recíproco de 4 es: "+miValParam.getValor(4.0));
        System.out.println("El recíproco de 5 es: "+miValParam.getValor(5.0));

        // Una expresión lambda debe ser compatible con el método definido en la interfaz funcional.
        // Por lo tanto, esto no funcionará:
        // miValor=()->"Tres"; // Error! String no compatible con double!
        // miValParam=()->Math.random(); // Error! Parámetro requerido!
    }
}

Salida:

Un valor constante: 28.6
El recíproco de 4 es: 0.25
El recíproco de 5 es: 0.2

Como se mencionó, la expresión lambda debe ser compatible con el método abstracto que se pretende implementar. Por este motivo, las líneas comentadas al final del programa anterior son ilegales. El primero, porque un valor de tipo String no es compatible con double, que es el tipo de devolución requerido por getValor(). El segundo, porque getValor(int) en miValParam requiere un parámetro y no se proporciona uno.

4.2. Ejemplo 2

Un aspecto clave de una interfaz funcional es que se puede usar con cualquier expresión lambda que sea compatible con ella. Por ejemplo, considere el siguiente programa.

Define una interfaz funcional llamada PruebaNum que declara el método abstracto prueba(). Este método tiene dos parámetros int y devuelve un resultado booleano. Su propósito es determinar si los dos argumentos pasados a prueba() satisfacen alguna condición. Devuelve el resultado de la prueba.

En main(), se crean tres pruebas diferentes mediante el uso de expresiones lambda. Una prueba si el primer argumento puede ser dividido por el segundo; el segundo determina si el primer argumento es menor que el segundo; y el tercero devuelve verdadero si los valores absolutos de los argumentos son iguales. Observe que las expresiones lambda que implementan estas pruebas tienen dos parámetros y devuelven un resultado boolean. Esto es, por supuesto, necesario ya que prueba() tiene dos parámetros y devuelve un resultado boolean.

// Uso de la misma interfaz funcional con tres expresiones lambda diferentes.
// Una interfaz funcional que toma dos parámetros int y devuelve un resultado booleano.

interface PruebaNum {
    boolean prueba(int n, int m);
}

class LambdaDemo{
    public static void main(String[] args) {

     // Esta expresión lambda determina si un número es un divisor de otro.
      PruebaNum esDivisor=(n,d)-> (n%d) == 0;

      if(esDivisor.prueba(10,2))
          System.out.println(("2 es un Divisor de 10"));
      if(!esDivisor.prueba(10,3))
          System.out.println(("3 NO es un Divisor de 10"));

        System.out.println();
      // Esta expresión lambda devuelve true si el primer argumento es menor que el segundo.
      PruebaNum menorQue= (n,m) -> (n<m);
       if (menorQue.prueba(2,10))
           System.out.println("2 es menor que 10");
       if (!menorQue.prueba(10,2))
           System.out.println("10 NO es menor que 2");

        System.out.println();
       // Esta expresión lambda devuelve true si los valores absolutos de los argumentos son iguales.
       PruebaNum igualAbs=(n,m) -> (n < 0 ? -n : n) == (m < 0 ? -m : m);

       if (igualAbs.prueba(4,-4))
           System.out.println("Valores absolutos de 4 y -4 son iguales");
       if (!igualAbs.prueba(4,5))
            System.out.println("Valores absolutos de 4 y -5 NO son iguales");
    }
}

Salida:

2 es un Divisor de 10
3 NO es un Divisor de 10

2 es menor que 10
10 NO es menor que 2

Valores absolutos de 4 y -4 son iguales
Valores absolutos de 4 y -5 NO son iguales

Como lo ilustra el programa, debido a que las tres expresiones lambda son compatibles con prueba(), todas se pueden ejecutar a través de una referencia PruebaNum. De hecho, no es necesario utilizar tres variables de referencia PruebaNum separadas, porque el mismo podría haberse utilizado para las tres pruebas. Por ejemplo, puede crear la variable miPrueba y luego usarla para referirse a cada prueba, a su vez, como se muestra aquí:

PruebaNum miPrueba;

        miPrueba=(n,d)-> (n%d) == 0;

        if(miPrueba.prueba(10,2))
            System.out.println(("2 es un Divisor de 10"));
        //...

        miPrueba= (n,m) -> (n<m);
        if (miPrueba.prueba(2,10))
            System.out.println("2 es menor que 10");
        //...

        miPrueba =(n,m) -> (n < 0 ? -n : n) == (m < 0 ? -m : m);

        if (miPrueba.prueba(4,-4))
            System.out.println("Valores absolutos de 4 y -4 son iguales");
        // ...

Por supuesto, el uso de diferentes variables de referencia llamadas esDivisor, menorQue e igualAbs, como lo hace el programa original, deja muy claro a qué expresión lambda se refiere cada variable.

Hay otro punto de interés en el programa anterior. Observe cómo se especifican los dos parámetros para las expresiones lambda. Por ejemplo, aquí está el que determina si un número es divisible de otro:

(n, d) -> (n % d) == 0

Observe que n y d están separados por comas. En general, cuando se requiere más de un parámetro, los parámetros se especifican, separados por comas, en una lista entre paréntesis en el lado izquierdo del operador lambda.

4.2. Ejemplo 3

Aunque los ejemplos anteriores utilizaron valores primitivos como los tipos de parámetros y el tipo de retorno del método abstracto definido por una interfaz funcional, no hay ninguna restricción al respecto.

Por ejemplo, el siguiente programa declara una interfaz funcional llamada PruebaString. Tiene un método llamado prueba() que toma dos parámetros de String y devuelve un resultado boolean. Por lo tanto, se puede usar para probar alguna condición relacionada con strings. Aquí, se crea una expresión lambda que determina si un String está contenida dentro de otra:

// Una interfaz funcional que prueba dos strings.

interface PruebaString {
    boolean prueba(String sa, String sb);
}

class LambdaDemo{
    public static void main(String[] args) {

      // Esta expresión lambda determina si un string es parte de otra.
      PruebaString esParte= (a,b)->a.indexOf(b) !=-1;

      String str="Esto es una prueba";

        System.out.println("Probando string: "+str);

        if (esParte.prueba(str,"es una"))
            System.out.println("'es una' encontrado");
        else
            System.out.println("'es una' NO encontrado");

        if (esParte.prueba(str,"xyz"))
            System.out.println("'xyz' encontrado");
        else
            System.out.println("'xyz' NO encontrado");
    }
}

Salida:

Probando string: Esto es una prueba
'es una' encontrado
'xyz' NO encontrado

Observe que la expresión lambda utiliza el método indexOf () definido por la clase String para determinar si una cadena/string es parte de otra. Esto funciona porque los parámetros a y b se determinan por inferencia de tipo para que sean del tipo String. Por lo tanto, es permisible llamar a un método String en a.

5. Bloque Expresiones Lambda

El cuerpo de las lambdas que se muestra en los ejemplos anteriores consiste en una sola expresión. Estos tipos de cuerpos lambda se denominan cuerpos de expresión, y las lambdas que tienen cuerpos de expresión a veces se denominan expresión lambdas. En un cuerpo de expresión, el código en el lado derecho del operador lambda debe consistir en una sola expresión, que se convierte en el valor de lambda. Aunque la expresión lambdas es bastante útil, a veces la situación requerirá más de una sola expresión.

Para manejar estos casos, Java admite un segundo tipo de expresión lambda en la cual el código en el lado derecho del operador lambda consiste en un bloque de código que puede contener más de una declaración. Este tipo de cuerpo lambda se llama cuerpo de bloque. Las lambdas que tienen cuerpos de bloque a veces se denominan bloque lambdas.

Un bloque lambda amplía los tipos de operaciones que se pueden manejar dentro de una expresión lambda porque permite que el cuerpo de la lambda contenga múltiples declaraciones. Por ejemplo, en un bloque lambda puede declarar variables, usar bucles, especificar sentencias if y switch, crear bloques anidados, y así sucesivamente. Un bloque lambda es fácil de crear. Simplemente encierre el cuerpo entre llaves como lo haría con cualquier otro bloque de declaraciones

Además de permitir declaraciones múltiples, las lambdas de bloque se usan de forma muy parecida a la expresión lambdas que acabamos de mencionar. Sin embargo, una diferencia clave es que debe usar explícitamente una declaración return para devolver un valor. Esto es necesario porque un bloque de cuerpo lambda no representa una sola expresión.

5.1. Ejemplo de bloque lambda

Aquí hay un ejemplo que usa un bloque lambda para encontrar el factor positivo más pequeño de un valor int. Utiliza una interfaz llamada FuncNum que tiene un método llamado func(), que toma un argumento int y devuelve un resultado int. Por lo tanto, FuncNum admite una función numérica en valores de tipo int.

// Un bloque lambda que encuentra el divisor positivo
// más pequeño de un valor int.

interface FuncNum {
    int func (int n);
}

class LambdaDemo{
    public static void main(String[] args) {
        // Este bloque lambda devuelve el divisor positivo más pequeño de un valor
        FuncNum divPeq= (n) ->{
            int res=1;

            // Obtenga el valor absoluto de n.
            n = n<0 ? -n:n;

            for (int i=2; i<=n/i;i++)
                if ((n%i)==0) {
                    res = i;
                    break;
                }
            return res;
        };
         System.out.println("El divisor más pequeño de 12 es: "+divPeq.func(12));
         System.out.println("El divisor más pequeño de 15 es: "+divPeq.func(-15));
    }
}

Salida:

El divisor más pequeño de 12 es: 2
El divisor más pequeño de 15 es: 3

En el programa, observe que el bloque lambda declara una variable llamada res, usa un bucle for y tiene una declaración return. Estos son legales dentro de un bloque de cuerpo lambda. En esencia, el cuerpo del bloque de un lambda es similar a un cuerpo de un método. Otro punto. Cuando se produce una declaración return dentro de una expresión lambda, simplemente causa un retorno del lambda.

Expresiones Lambda en Java
  • Introducción, Fundamentos y Ejemplos

Sobre el Autor:

Hey hola! Yo soy Alex Walton y tengo el placer de compartir conocimientos hacía ti sobre el tema de Programación en Java, desde cero, Online y Gratis.

Deja una Respuesta

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.