Expresiones Lambda en Java

    Introducción

    Las funciones lambda han sido una adición que vino con Java 8, y fue el primer paso del lenguaje hacia programación funcional, siguiendo una tendencia general hacia la implementación de funciones útiles de varios paradigmas.

    La motivación para introducir funciones lambda fue principalmente reducir el engorroso código repetitivo que se utilizaba para pasar instancias de clases para simular funciones anónimas de otros lenguajes.

    He aquí un ejemplo:

    String[] arr = { "family", "illegibly", "acquired", "know", "perplexing", "do", "not", "doctors", "where", "handwriting", "I" };
    
    Arrays.sort(arr, new Comparator<String>() {
        @Override public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    });
    
    System.out.println(Arrays.toString(arr));
    

    Como puede ver, toda la instancia de una nueva clase Comparator y anular su contenido es un fragmento de código repetitivo del que también podemos prescindir, ya que siempre es el mismo.

    La totalidad Arrays.sort() La línea se puede reemplazar por algo mucho más corto y dulce, pero funcionalmente equivalente:

    Arrays.sort(arr, (s1,s2) -> s1.length() - s2.length());
    

    Estos fragmentos de código breves y dulces que hacen lo mismo que sus homólogos detallados se denominan azúcar sintáctica. Esto se debe a que no agregan funcionalidad a un idioma, sino que lo hacen más compacto y legible. Las funciones Lambda son un ejemplo de azúcar sintáctico para Java.

    Aunque recomiendo encarecidamente leer este artículo en orden, si no está familiarizado con el tema, aquí hay una lista rápida de lo que cubriremos para una referencia más fácil:

    • Lambdas como objetos
      • Coincidencia de interfaz de método único
    • Implementación
      • Parámetros
      • Cuerpo
      • Captura variable
      • Referencia de método
        • Referencia de método estático
        • Referencia del método de parámetro
        • Referencia del método de instancia
        • Referencia del método de constructor

    Lambdas como objetos

    Antes de entrar en el meollo de la sintaxis lambda en sí, deberíamos echar un vistazo a las funciones lambda en primer lugar y cómo se utilizan.

    Como se mencionó, son simplemente azúcar sintáctico, pero son azúcar sintáctico específicamente para objetos que implementan una interfaz de método único.

    En esos objetos, la implementación lambda se considera la implementación de dicho método. Si la lambda y la interfaz coinciden, la función lambda se puede asignar a una variable del tipo de esa interfaz.

    Coincidencia de interfaz de método único

    Para hacer coincidir una lambda con una interfaz de método único, también llamada “interfaz funcional”, se deben cumplir varias condiciones:

    • La interfaz funcional debe tener exactamente un método no implementado, y ese método (naturalmente) debe ser abstracto. La interfaz puede contener métodos estáticos y predeterminados implementados dentro de ella, pero lo importante es que hay exactamente un método abstracto.
    • El método abstracto tiene que aceptar argumentos, en el mismo orden, que correspondan a los parámetros que acepta lambda.
    • El tipo de retorno tanto del método como de la función lambda deben coincidir.

    Si todo eso está satisfecho, se han realizado todas las condiciones para la coincidencia y puede asignar su lambda a la variable.

    Definamos nuestra interfaz:

    public interface HelloWorld {
        abstract void world();
    }
    

    Como puede ver, tenemos una interfaz funcional bastante inútil.

    Contiene exactamente una función, y esa función puede hacer cualquier cosa, siempre que no acepte argumentos y no devuelva valores.

    Vamos a hacer un programa simple de Hello World usando esto, aunque la imaginación es el límite si quieres jugar con él:

    public class Main {
        public static void main(String[] args) {
            HelloWorld hello = () -> System.out.println("Hello World!");
            hello.world();
        }
    }
    

    Como podemos ver si ejecutamos esto, nuestra función lambda ha coincidido con éxito con el HelloWorld interfaz y el objeto hello ahora se puede utilizar para acceder a su método.

    La idea detrás de esto es que puede usar lambdas donde de otra manera usaría interfaces funcionales para transmitir funciones. Si reStrings nuestro Comparator ejemplo, Comparator<T> es en realidad una interfaz funcional que implementa un método único: compare().

    Es por eso que podríamos reemplazarlo con una lambda que se comporte de manera similar a ese método.

    Implementación

    La idea básica detrás de las funciones lambda es la misma que la idea básica detrás de los métodos: toman parámetros y los usan dentro del cuerpo que consta de expresiones.

    La implementación es un poco diferente. Tomemos el ejemplo de nuestro String clasificación de lambda:

    (s1,s2) -> s1.length() - s2.length()
    

    Su sintaxis puede entenderse como:

    parameters -> body
    

    Parámetros

    Parámetros son los mismos que los parámetros de la función, esos son valores pasados ​​a una función lambda para que haga algo.

    Los parámetros suelen estar entre corchetes y separados por comas, aunque en el caso de una lambda, que recibe solo un parámetro, los corchetes se pueden omitir.

    Una función lambda puede tomar cualquier cantidad de parámetros, incluido cero, por lo que podría tener algo como esto:

    () -> System.out.println("Hello World!")
    

    Esta función lambda, cuando se combina con una interfaz correspondiente, funcionará igual que la siguiente función:

    static void printing(){
        System.out.println("Hello World!");
    }
    

    De manera similar, podemos tener funciones lambda con uno, dos o más parámetros.

    Un ejemplo clásico de una función con un parámetro es trabajar en cada elemento de una colección en un forEach lazo:

    public class Main {
        public static void main(String[] args) {
            LinkedList<Integer> childrenAges = new LinkedList<Integer>(Arrays.asList(2, 4, 5, 7));
            childrenAges.forEach( age -> System.out.println("One of the children is " + age + " years old."));
        }
    }
    

    Aquí, el único parámetro es age. Tenga en cuenta que eliminamos los paréntesis a su alrededor aquí, porque está permitido cuando solo tenemos un parámetro.

    El uso de más parámetros funciona de manera similar, solo están separados por una coma y encerrados entre paréntesis. Ya hemos visto lambda de dos parámetros cuando lo emparejamos con Comparator para ordenar las cadenas.

    Cuerpo

    Un cuerpo de una expresión lambda consta de una sola expresión o un bloque de instrucciones.

    Si especifica solo una única expresión como el cuerpo de una función lambda (ya sea en un bloque de instrucciones o por sí misma), la lambda devolverá automáticamente la evaluación de esa expresión.

    Si tiene varias líneas en su bloque de declaraciones, o si simplemente lo desea (es un país libre), puede usar explícitamente una declaración de retorno desde dentro de un bloque de declaraciones:

    // just the expression
    (s1,s2) -> s1.length() - s2.length()
    
    // statement block
    (s1,s2) -> { s1.length() - s2.length(); }
    
    // using return
    (s1,s2) -> {
        s1.length() - s2.length();
        return; // because forEach expects void return
    }
    

    Puede intentar sustituir cualquiera de estos en nuestro ejemplo de clasificación al principio del artículo, y encontrará que todos funcionan exactamente igual.

    Captura variable

    La captura de variables permite que las lambdas utilicen variables declaradas fuera de la propia lambda.

    Hay tres tipos de captura de variables muy similares:

    • captura de variable local
    • captura de variable de instancia
    • captura de variable estática

    La sintaxis es casi idéntica a cómo accedería a estas variables desde cualquier otra función, pero las condiciones bajo las cuales puede hacerlo son diferentes.

    Puede acceder a variable local solo si es efectivamente final, lo que significa que no cambia su valor después de la asignación. No tiene que declararse explícitamente como final, pero es recomendable hacerlo para evitar confusiones. Si lo usa en una función lambda y luego cambia su valor, el compilador comenzará a quejarse.

    La razón por la que no puede hacer esto es porque la lambda no puede hacer referencia de manera confiable a una variable local, porque puede destruirse antes de ejecutar la lambda. Debido a esto, hace un copia profunda. Cambiar la variable local puede llevar a un comportamiento confuso, ya que el programador puede esperar que cambie el valor dentro de la lambda, por lo que para evitar confusiones, está explícitamente prohibido.

    Cuando se trata de variables de instancia, si su lambda está dentro de la misma clase que la variable a la que está accediendo, simplemente puede usar this.field para acceder a un campo en esa clase. Además, el campo no tiene que ser definitivo y puede modificarse posteriormente durante el transcurso del programa.

    Esto se debe a que si una lambda se define dentro de una clase, se instancia junto con esa clase y se vincula a esa instancia de clase, por lo que puede hacer referencia fácilmente al valor del campo que necesita.

    Variables estáticas se capturan de manera muy similar a las variables de instancia, excepto por el hecho de que no usarías this para referirse a ellos. Se pueden cambiar y no es necesario que sean definitivos por las mismas razones.

    Referencia de método

    A veces, las lambdas son solo sustitutos de un método específico. Con el ánimo de hacer que la sintaxis sea breve y sencilla, no es necesario que mecanografíe toda la sintaxis cuando ese sea el caso. Por ejemplo:

    s -> System.out.println(s)
    

    es equivalente a:

    System.out::println
    

    los :: la sintaxis le permitirá al compilador saber que solo desea una lambda que pase el argumento dado a println. Siempre debes anteponer el nombre del método con :: donde escribiría una función lambda, de lo contrario accedería al método como lo haría normalmente, lo que significa que aún debe especificar la clase propietaria antes de los dos puntos dobles.

    Hay varios tipos de referencias a métodos, según el tipo de método al que llame:

    • referencia de método estático
    • referencia de método de parámetro
    • referencia de método de instancia
    • referencia al método constructor
    Referencia de método estático

    Necesitamos una interfaz:

    public interface Average {
        abstract double average(double a, double b);
    }
    

    Una función estática:

    public class LambdaFunctions {
        static double averageOfTwo(double a, double b){
            return (a+b)/2;
        }
    }
    

    Y nuestra función lambda y llamar main:

    Average avg = LambdaFunctions::averageOfTwo;
    System.out.println(avg.average(20.3, 4.5));
    
    Referencia del método de parámetro

    De nuevo, estamos escribiendo main.

    Comparator<Double> cmp = Double::compareTo;
    Double a = 20.3;
    System.out.println(cmp.compare(a, 4.5));
    

    los Double::compareTo lambda es equivalente a:

    Comparator<Double> cmp = (a, b) -> a.compareTo(b)
    
    Referencia del método de instancia

    Si tomamos nuestro LambdaFunctions clase y nuestra función averageOfTwo (de la Referencia del método estático) y hacerlo no estático, obtendremos lo siguiente:

    public class LambdaFunctions {
        double averageOfTwo(double a, double b){
            return (a+b)/2;
        }
    }
    

    Para acceder a esto, ahora necesitamos una instancia de la clase, por lo que tendríamos que hacer esto en main:

    LambdaFunctions lambda = new LambdaFunctions();
    Average avg = lambda::averageOfTwo;
    System.out.println(avg.average(20.3, 4.5));
    
    Referencia del método de constructor

    Si tenemos una clase llamada MyClass y quiere llamar a su constructor a través de una función lambda, nuestra lambda se verá así:

    MyClass::new
    

    Aceptará tantos argumentos como pueda coincidir con uno de los constructores.

    Conclusión

    En conclusión, las lambdas son una característica útil para hacer nuestro código más simple, más corto y más legible.

    Algunas personas evitan usarlos cuando hay muchos Juniors en el equipo, por lo que aconsejaría consultar con su equipo antes de refactorizar todo su código, pero cuando todos están en la misma página, son una gran herramienta.

    Ver también

    Aquí hay más información sobre cómo y dónde aplicar las funciones lambda:

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *