CodeGym/Blog Java/Random-ES/Una explicación de las expresiones lambda en Java. Con ej...
John Squirrels
Nivel 41
San Francisco

Una explicación de las expresiones lambda en Java. Con ejemplos y tareas. Parte 1

Publicado en el grupo Random-ES
¿Para quién es este artículo?
  • Es para personas que creen que ya conocen bien Java Core, pero no tienen idea de las expresiones lambda en Java. O tal vez hayan escuchado algo sobre las expresiones lambda, pero faltan los detalles.
  • Es para personas que tienen una cierta comprensión de las expresiones lambda, pero aún se sienten intimidadas por ellas y no están acostumbradas a usarlas.
Una explicación de las expresiones lambda en Java.  Con ejemplos y tareas.  Parte 1 - 1Si no encaja en una de estas categorías, es posible que encuentre este artículo aburrido, defectuoso o, en general, que no sea de su agrado. En este caso, siéntase libre de pasar a otras cosas o, si está bien versado en el tema, haga sugerencias en los comentarios sobre cómo podría mejorar o complementar el artículo. El material no pretende tener ningún valor académico, y mucho menos novedad. Todo lo contrario: intentaré describir cosas que son complejas (para algunas personas) de la forma más sencilla posible. Una solicitud para explicar la API de Stream me inspiró a escribir esto. Lo pensé y decidí que algunos de mis ejemplos de transmisión serían incomprensibles sin una comprensión de las expresiones lambda. Entonces, comenzaremos con expresiones lambda. ¿Qué necesitas saber para entender este artículo?
  1. Debe comprender la programación orientada a objetos (POO), a saber:

    • clases, objetos y la diferencia entre ellos;
    • interfaces, cómo se diferencian de las clases y relación entre interfaces y clases;
    • métodos, cómo llamarlos, métodos abstractos (es decir, métodos sin implementación), parámetros de métodos, argumentos de métodos y cómo pasarlos;
    • modificadores de acceso, métodos/variables estáticos, métodos/variables finales;
    • herencia de clases e interfaces, herencia múltiple de interfaces.
  2. Conocimiento de Java Core: tipos genéricos (genéricos), colecciones (listas), hilos.
Bueno, vamos a ello.

Una pequeña historia

Las expresiones lambda llegaron a Java desde la programación funcional y luego desde las matemáticas. En Estados Unidos a mediados del siglo XX, Alonzo Church, muy aficionado a las matemáticas y todo tipo de abstracciones, trabajaba en la Universidad de Princeton. Fue Alonzo Church quien inventó el cálculo lambda, que inicialmente era un conjunto de ideas abstractas que no tenían nada que ver con la programación. Matemáticos como Alan Turing y John von Neumann trabajaron en la Universidad de Princeton al mismo tiempo. Todo salió bien: a Church se le ocurrió el cálculo lambda. Turing desarrolló su máquina de computación abstracta, ahora conocida como la "máquina de Turing". Y von Neumann propuso una arquitectura de computadora que ha formado la base de las computadoras modernas (ahora llamada "arquitectura de von Neumann"). En ese momento, la Iglesia de Alonso' Sus ideas no llegaron a ser tan conocidas como los trabajos de sus colegas (con la excepción del campo de las matemáticas puras). Sin embargo, un poco más tarde, John McCarthy (también graduado de la Universidad de Princeton y, en el momento de nuestra historia, empleado del Instituto Tecnológico de Massachusetts) se interesó en las ideas de Church. En 1958, creó el primer lenguaje de programación funcional, LISP, basado en esas ideas. Y 58 años después, las ideas de la programación funcional se filtraron en Java 8. No han pasado ni 70 años... Honestamente, esto no es lo más que se ha tardado en aplicar una idea matemática en la práctica. un empleado del Instituto de Tecnología de Massachusetts) se interesó en las ideas de Church. En 1958, creó el primer lenguaje de programación funcional, LISP, basado en esas ideas. Y 58 años después, las ideas de la programación funcional se filtraron en Java 8. No han pasado ni 70 años... Honestamente, esto no es lo más que se ha tardado en aplicar una idea matemática en la práctica. un empleado del Instituto de Tecnología de Massachusetts) se interesó en las ideas de Church. En 1958, creó el primer lenguaje de programación funcional, LISP, basado en esas ideas. Y 58 años después, las ideas de la programación funcional se filtraron en Java 8. No han pasado ni 70 años... Honestamente, esto no es lo más que se ha tardado en aplicar una idea matemática en la práctica.

Lo importante del asunto

Una expresión lambda es un tipo de función. Puede considerarlo como un método Java ordinario pero con la capacidad distintiva de pasarse a otros métodos como argumento. Así es. ¡Se ha vuelto posible pasar no solo números, cadenas y gatos a métodos, sino también otros métodos! ¿Cuándo podríamos necesitar esto? Sería útil, por ejemplo, si queremos pasar algún método de devolución de llamada. Es decir, si necesitamos que el método que llamamos tenga la capacidad de llamar a algún otro método que le pasemos. En otras palabras, tenemos la capacidad de pasar una devolución de llamada en ciertas circunstancias y una devolución de llamada diferente en otras. Y para que nuestro método que recibe nuestras devoluciones de llamada las llame. La clasificación es un ejemplo simple. Supongamos que estamos escribiendo un algoritmo de clasificación inteligente que se ve así:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
En la ifdeclaración, llamamos al compare()método, pasando dos objetos para comparar, y queremos saber cuál de estos objetos es "mayor". Suponemos que el "mayor" viene antes que el "menor". Pongo "mayor" entre comillas, porque estamos escribiendo un método universal que sabrá cómo ordenar no solo en orden ascendente, sino también en orden descendente (en este caso, el objeto "mayor" será en realidad el objeto "menor" , y viceversa). Para establecer el algoritmo específico para nuestro tipo, necesitamos algún mecanismo para pasarlo a nuestro mySuperSort()método. De esa forma podremos "controlar" nuestro método cuando se llame. Por supuesto, podríamos escribir dos métodos separados, mySuperSortAscend()ymySuperSortDescend()— para clasificar en orden ascendente y descendente. O podríamos pasar algún argumento al método (por ejemplo, una variable booleana; si es verdadero, ordenar en orden ascendente, y si es falso, entonces en orden descendente). Pero, ¿qué pasa si queremos ordenar algo complicado, como una lista de arreglos de cadenas? ¿ Cómo mySuperSort()sabrá nuestro método cómo ordenar estas matrices de cadenas? ¿Por tamaño? ¿Por la longitud acumulada de todas las palabras? ¿Quizás alfabéticamente según la primera cadena de la matriz? ¿Y si necesitamos ordenar la lista de matrices por tamaño de matriz en algunos casos y por la longitud acumulada de todas las palabras en cada matriz en otros casos? Espero que ya haya oído hablar de los comparadores y que, en este caso, simplemente pasaríamos a nuestro método de clasificación un objeto comparador que describe el algoritmo de clasificación deseado. porque la normasort()El método se implementa según el mismo principio que mySuperSort()usaré sort()en mis ejemplos.
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<;String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() {

    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }

        for (String s : o2) {
            length2 += s.length();
        }

        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Resultado:
Dota GTA5 Halo
if then else
I really love Java
Aquí las matrices se ordenan por el número de palabras en cada matriz. Una matriz con menos palabras se considera "menor". Por eso es lo primero. Una matriz con más palabras se considera "mayor" y se coloca al final. Si pasamos un comparador diferente al sort()método, como sortByCumulativeWordLength, obtendremos un resultado diferente:
if then else
Dota GTA5 Halo
I really love Java
Ahora las matrices están ordenadas por el número total de letras en las palabras de la matriz. En la primera matriz, hay 10 letras, en la segunda, 12 y en la tercera, 15. Si solo tenemos un único comparador, entonces no tenemos que declarar una variable separada para él. En su lugar, podemos simplemente crear una clase anónima justo en el momento de la llamada al sort()método. Algo como esto:
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();

arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Obtendremos el mismo resultado que en el primer caso. Tarea 1. Vuelva a escribir este ejemplo para que ordene las matrices no en orden ascendente del número de palabras en cada matriz, sino en orden descendente. Ya sabemos todo esto. Sabemos cómo pasar objetos a métodos. Dependiendo de lo que necesitemos en ese momento, podemos pasar diferentes objetos a un método, que luego invocará el método que implementamos. Esto plantea la pregunta: ¿por qué demonios necesitamos una expresión lambda aquí?  Porque una expresión lambda es un objeto que tiene exactamente un método. Como un "objeto de método". Un método empaquetado en un objeto. Simplemente tiene una sintaxis un poco desconocida (pero más sobre eso más adelante). Echemos otro vistazo a este código:
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Aquí tomamos nuestra lista de arreglos y llamamos a su sort()método, al que le pasamos un objeto comparador con un solo compare()método (su nombre no nos importa; después de todo, es el único método de este objeto, por lo que no podemos equivocarnos). Este método tiene dos parámetros con los que trabajaremos. Si está trabajando en IntelliJ IDEA, probablemente vio que ofrece condensar significativamente el código de la siguiente manera:
arrays.sort((o1, o2) -> o1.length - o2.length);
Esto reduce seis líneas a una sola corta. 6 líneas se reescriben como una sola. Algo desapareció, pero te garantizo que no era nada importante. Este código funcionará exactamente de la misma manera que lo haría con una clase anónima. Tarea 2. Intente reescribir la solución de la Tarea 1 usando una expresión lambda (como mínimo, solicite a IntelliJ IDEA que convierta su clase anónima en una expresión lambda).

Hablemos de interfaces

En principio, una interfaz es simplemente una lista de métodos abstractos. Cuando creamos una clase que implementa alguna interfaz, nuestra clase debe implementar los métodos incluidos en la interfaz (o tenemos que hacer que la clase sea abstracta). Hay interfaces con muchos métodos diferentes (por ejemplo,  List), y hay interfaces con un solo método (por ejemplo, Comparatoro Runnable). Hay interfaces que no tienen un solo método (las llamadas interfaces de marcador como Serializable). Las interfaces que tienen un solo método también se denominan interfaces funcionales . En Java 8, incluso están marcados con una anotación especial:@FunctionalInterface. Son estas interfaces de método único las que son adecuadas como tipos de destino para las expresiones lambda. Como dije anteriormente, una expresión lambda es un método envuelto en un objeto. Y cuando pasamos un objeto de este tipo, esencialmente estamos pasando este único método. Resulta que no nos importa cómo se llame el método. Lo único que nos importa son los parámetros del método y, por supuesto, el cuerpo del método. En esencia, una expresión lambda es la implementación de una interfaz funcional. Dondequiera que veamos una interfaz con un solo método, una clase anónima se puede reescribir como una lambda. Si la interfaz tiene más o menos de un método, entonces una expresión lambda no funcionará y, en su lugar, usaremos una clase anónima o incluso una instancia de una clase ordinaria. Ahora es el momento de profundizar un poco en las lambdas. :)

Sintaxis

La sintaxis general es algo como esto:
(parameters) -> {method body}
Es decir, paréntesis alrededor de los parámetros del método, una "flecha" (formada por un guión y un signo mayor que), y luego el cuerpo del método entre llaves, como siempre. Los parámetros corresponden a los especificados en el método de interfaz. Si el compilador puede determinar sin ambigüedades los tipos de variables (en nuestro caso, sabe que estamos trabajando con matrices de cadenas, porque nuestro Listobjeto se escribe usando String[]), entonces no es necesario que indique sus tipos.
Si son ambiguos, indique el tipo. IDEA lo coloreará de gris si no es necesario.
Puede leer más en este tutorial de Oracle y en otros lugares. Esto se llama " tipificación de objetivos ". Puede nombrar las variables como desee, no tiene que usar los mismos nombres especificados en la interfaz. Si no hay parámetros, simplemente indique paréntesis vacíos. Si solo hay un parámetro, simplemente indique el nombre de la variable sin paréntesis. Ahora que comprendemos los parámetros, es hora de analizar el cuerpo de la expresión lambda. Dentro de las llaves, escribes el código como lo harías con un método normal. Si su código consta de una sola línea, puede omitir las llaves por completo (similar a las declaraciones if y los bucles for). Si su lambda de una sola línea devuelve algo, no tiene que incluir unreturndeclaración. Pero si usa llaves, entonces debe incluir explícitamente una returndeclaración, tal como lo haría en un método ordinario.

Ejemplos

Ejemplo 1.
() -> {}
El ejemplo más simple. Y lo más inútil :), ya que no hace nada. Ejemplo 2.
() -> ""
Otro ejemplo interesante. No toma nada y devuelve una cadena vacía ( returnse omite porque no es necesario). Aquí está lo mismo, pero con return:
() -> {
    return "";
}
Ejemplo 3. "¡Hola, mundo!" usando lambdas
() -> System.out.println("Hello, World!")
No toma nada y no devuelve nada (no podemos ponerlo returnantes de la llamada a System.out.println(), porque el println()tipo de retorno del método es void). Simplemente muestra el saludo. Esto es ideal para una implementación de la Runnableinterfaz. El siguiente ejemplo es más completo:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello, World!")).start();
    }
}
O así:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello, World!"));
        t.start();
    }
}
O incluso podemos guardar la expresión lambda como un Runnableobjeto y luego pasarla al Threadconstructor:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello, World!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Echemos un vistazo más de cerca al momento en que una expresión lambda se guarda en una variable. La Runnableinterfaz nos dice que sus objetos deben tener un public void run()método. De acuerdo con la interfaz, el runmétodo no toma parámetros. Y no devuelve nada, es decir, su tipo de retorno es void. En consecuencia, este código creará un objeto con un método que no toma ni devuelve nada. Esto coincide perfectamente con el método Runnablede la interfaz run(). Es por eso que pudimos poner esta expresión lambda en una Runnablevariable.  Ejemplo 4.
() -> 42
Nuevamente, no toma nada, pero devuelve el número 42. Tal expresión lambda se puede poner en una Callablevariable, porque esta interfaz tiene solo un método que se parece a esto:
V call(),
donde  V es el tipo de devolución (en nuestro caso,  int). En consecuencia, podemos guardar una expresión lambda de la siguiente manera:
Callable<Integer> c = () -> 42;
Ejemplo 5. Una expresión lambda que involucra varias líneas
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Nuevamente, esta es una expresión lambda sin parámetros y un voidtipo de retorno (porque no hay returndeclaración).  Ejemplo 6
x -> x
Aquí tomamos una xvariable y la devolvemos. Tenga en cuenta que si solo hay un parámetro, puede omitir los paréntesis que lo rodean. Esto es lo mismo, pero con paréntesis:
(x) -> x
Y aquí hay un ejemplo con una declaración de retorno explícita:
x -> {
    return x;
}
O así con paréntesis y una declaración de retorno:
(x) -> {
    return x;
}
O con una indicación explícita del tipo (y por lo tanto entre paréntesis):
(int x) -> x
Ejemplo 7
x -> ++x
Lo tomamos xy lo devolvemos, pero solo después de agregar 1. Puedes reescribir esa lambda así:
x -> x + 1
En ambos casos, omitimos los paréntesis alrededor del cuerpo del parámetro y del método, junto con la returndeclaración, ya que son opcionales. Las versiones con paréntesis y una declaración de retorno se dan en el Ejemplo 6. Ejemplo 8
(x, y) -> x % y
Tomamos xy yy devolvemos el resto de la división de xpor y. Los paréntesis alrededor de los parámetros son obligatorios aquí. Son opcionales solo cuando hay un solo parámetro. Aquí está con una indicación explícita de los tipos:
(double x, int y) -> x % y
Ejemplo 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Tomamos un Catobjeto, un Stringnombre y una edad int. En el método en sí, usamos el nombre y la edad pasados ​​para establecer variables en el gato. Debido a que nuestro catobjeto es un tipo de referencia, se cambiará fuera de la expresión lambda (obtendrá el nombre y la edad pasados). Aquí hay una versión un poco más complicada que usa una lambda similar:
public class Main {

    public static void main(String[] args) {
        // Create a cat and display it to confirm that it is "empty"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // Create a lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);

        };

        // Call a method to which we pass the cat and lambda
        changeEntity(myCat, s);

        // Display the cat on the screen and see that its state has changed (it has a name and age)
        System.out.println(myCat);

    }

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Smokey", 3);
    }
}

interface HasNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends HasNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements HasNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Resultado:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Como puede ver, el Catobjeto tenía un estado y luego el estado cambió después de que usamos la expresión lambda. Las expresiones lambda combinan perfectamente con los genéricos. Y si necesitamos crear una Dogclase que también implemente HasNameAndAge, podemos realizar las mismas operaciones en Dogel main() método sin cambiar la expresión lambda. Tarea 3. Escriba una interfaz funcional con un método que tome un número y devuelva un valor booleano. Escriba una implementación de dicha interfaz como una expresión lambda que devuelva verdadero si el número pasado es divisible por 13. Tarea 4.Escriba una interfaz funcional con un método que tome dos cadenas y también devuelva una cadena. Escriba una implementación de dicha interfaz como una expresión lambda que devuelva la cadena más larga. Tarea 5. Escriba una interfaz funcional con un método que tome tres números de punto flotante: a, b y c y también devuelva un número de punto flotante. Escriba una implementación de dicha interfaz como una expresión lambda que devuelva el discriminante. En caso de que lo hayas olvidado, eso es D = b^2 — 4ac. Tarea 6. Utilizando la interfaz funcional de la Tarea 5, escriba una expresión lambda que devuelva el resultado de a * b^c. Una explicación de las expresiones lambda en Java. Con ejemplos y tareas. Parte 2
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios