JavaRush /Blog Java /Random-ES /Popular sobre las expresiones lambda en Java. Con ejemplo...
Стас Пасинков
Nivel 26
Киев

Popular sobre las expresiones lambda en Java. Con ejemplos y tareas. Parte 1

Publicado en el grupo Random-ES
¿Para quién es este artículo?
  • Para aquellos que creen que ya conocen bien Java Core, pero no tienen idea de las expresiones lambda en Java. O quizás ya hayas oído algo sobre lambdas, pero sin detalles.
  • para aquellos que tienen algún conocimiento de las expresiones lambda, pero todavía tienen miedo y son inusuales al usarlas.
Si no pertenece a ninguna de estas categorías, es posible que este artículo le resulte aburrido, incorrecto y, en general, "no interesante". En este caso, no dude en pasar por alto o, si conoce bien el tema, sugiera en los comentarios cómo podría mejorar o complementar el artículo. El material no pretende ningún valor académico, y mucho menos novedad. Más bien, al contrario: en él intentaré describir cosas complejas (para algunos) de la forma más sencilla posible. Me inspiré para escribir en una solicitud para explicar la API de transmisión. Lo pensé y decidí que sin entender las expresiones lambda, algunos de mis ejemplos sobre "corrientes" serían incomprensibles. Entonces comencemos con lambdas. Popular sobre las expresiones lambda en Java.  Con ejemplos y tareas.  Parte 1 - 1Qué conocimientos se requieren para comprender este artículo:
  1. Comprensión de la programación orientada a objetos (en lo sucesivo, POO), a saber:
    • conocimiento de qué son clases y objetos, cuál es la diferencia entre ellos;
    • conocimiento de qué son las interfaces, en qué se diferencian de las clases, cuál es la conexión entre ellas (interfaces y clases);
    • conocimiento de qué es un método, cómo llamarlo, qué es un método abstracto (o un método sin implementación), cuáles son los parámetros/argumentos de un método, cómo pasarlos allí;
    • modificadores de acceso, métodos/variables estáticos, métodos/variables finales;
    • herencia (clases, interfaces, herencia múltiple de interfaces).
  2. Conocimientos de Java Core: genéricos, colecciones (listas), threads.
Bueno, comencemos.

Una pequeña historia

Las expresiones lambda llegaron a Java desde la programación funcional y allí desde las matemáticas. A mediados del siglo XX en América trabajaba en la Universidad de Princeton un tal Alonzo Church, muy aficionado a las matemáticas y todo tipo de abstracciones. Fue Alonzo Church a quien se le ocurrió el cálculo lambda, que al principio era un conjunto de algunas ideas abstractas y no tenía nada que ver con la programación. Al mismo tiempo, en la misma Universidad de Princeton trabajaban matemáticos como Alan Turing y John von Neumann. Todo salió bien: a Church se le ocurrió el sistema de cálculo lambda, Turing desarrolló su máquina informática abstracta, ahora conocida como la “máquina de Turing”. Bueno, von Neumann propuso un diagrama de la arquitectura de las computadoras, que formó la base de las computadoras modernas (y ahora se llama "arquitectura de von Neumann"). En ese momento, las ideas de Alonzo Church no ganaron tanta fama como el trabajo de sus colegas (con excepción del campo de las matemáticas “puras”). Sin embargo, un poco más tarde, un tal John McCarthy (también graduado de la Universidad de Princeton, en el momento de la historia, empleado del Instituto Tecnológico de Massachusetts) se interesó por las ideas de Church. A partir de ellos, en 1958 creó el primer lenguaje de programación funcional, Lisp. Y 58 años después, las ideas de programación funcional se filtraron en Java como el número 8. No han pasado ni 70 años... De hecho, este no es el período de tiempo más largo para aplicar una idea matemática en la práctica.

La esencia

Una expresión lambda es una de esas funciones. Puedes considerarlo como un método normal en Java, la única diferencia es que se puede pasar a otros métodos como argumento. Sí, ahora es posible pasar no sólo números, cadenas y gatos a métodos, ¡sino también otros métodos! ¿Cuándo podríamos necesitar esto? Por ejemplo, si queremos pasar alguna devolución de llamada. Necesitamos que el método que llamamos pueda llamar a algún otro método que le pasemos. Es decir, para que tengamos la oportunidad de transmitir una devolución de llamada en unos casos y otra en otros. Y nuestro método, que aceptaría nuestras devoluciones de llamada, las llamaría. Un ejemplo sencillo es la clasificación. Digamos que escribimos algún tipo de clasificación complicada que se parece a esta:
public void mySuperSort() {
    // ... haz algo aquí
    if(compare(obj1, obj2) > 0)
    // ... y aquí hacemos algo
}
Donde ifllamamos al método compare(), pasamos dos objetos que comparamos y queremos saber cuál de estos objetos es "mayor". Pondremos el que es “más” antes del que es “menor”. Escribí “más” entre comillas porque estamos escribiendo un método universal que podrá ordenar no solo en orden ascendente sino también descendente (en este caso, “más” será el objeto que es esencialmente más pequeño, y viceversa) . Para establecer la regla de cómo queremos ordenar exactamente, debemos pasarla de alguna manera a nuestro archivo mySuperSort(). En este caso, podremos “controlar” de alguna manera nuestro método mientras se llama. Por supuesto, puede escribir dos métodos separados mySuperSortAsc()para mySuperSortDesc()ordenar en orden ascendente y descendente. O pase algún parámetro dentro del método (por ejemplo, booleanif true, ordene en orden ascendente y if falseen orden descendente). Pero, ¿qué pasa si no queremos ordenar una estructura simple, sino, por ejemplo, una lista de matrices de cadenas? ¿ Cómo mySuperSort()sabrá nuestro método cómo ordenar estas matrices de cadenas? ¿Medir? ¿Por longitud total de palabras? ¿Quizás alfabéticamente, dependiendo de la primera fila de la matriz? Pero, ¿qué pasa si, en algunos casos, necesitamos ordenar una lista de matrices por el tamaño de la matriz y, en otro caso, por la longitud total de las palabras de la matriz? Creo que ya has oído hablar de los comparadores y que en tales casos simplemente pasamos un objeto comparador a nuestro método de clasificación, en el que describimos las reglas por las cuales queremos ordenar. Dado que el método estándar sort()se implementa según el mismo principio que , mySuperSort()en los ejemplos usaré el estándar sort().
String[] array1 = {"Madre", "jabón", "marco"};
String[] array2 = {"I", "Muy", "Amo", "java"};
String[] array3 = {"mundo", "trabajar", "Puede"};

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[]> sortByWordsLength = 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:
  1. mamá lavó el marco
  2. paz laborista puede
  3. Realmente amo java
Aquí las matrices están ordenadas por la cantidad de palabras en cada matriz. Una matriz con menos palabras se considera "más pequeña". Por eso viene al principio. Aquel en el que hay más palabras se considera “más” y acaba al final. Si sort()le pasamos otro comparador al método (sortByWordsLength), entonces el resultado será diferente:
  1. paz laborista puede
  2. mamá lavó el marco
  3. Realmente amo java
Ahora las matrices se ordenan por el número total de letras de las palabras de dicha matriz. En el primer caso hay 10 letras, en el segundo 12 y en el tercero 15. Si usamos solo un comparador, entonces no podemos crear una variable separada para él, sino simplemente crear un objeto de una clase anónima justo en el momento de llamar al método sort(). Como eso:
String[] array1 = {"Madre", "jabón", "marco"};
String[] array2 = {"I", "Muy", "Amo", "java"};
String[] array3 = {"mundo", "trabajar", "Puede"};

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;
    }
});
El resultado será el mismo que en el primer caso. Tarea 1 . Vuelva a escribir este ejemplo para que ordene las matrices no en orden ascendente según el número de palabras en la matriz, sino en orden descendente. Todo esto ya lo sabemos. Sabemos cómo pasar objetos a métodos, podemos pasar tal o cual objeto a un método dependiendo de lo que necesitemos en ese momento, y dentro del método donde pasamos dicho objeto, se llamará el método para el cual escribimos la implementación. . Surge la pregunta: ¿qué tienen que ver las expresiones lambda con esto? Dado que una lambda es un objeto que contiene exactamente un método. Es como un objeto de método. Un método envuelto en un objeto. Simplemente tienen una sintaxis ligeramente inusual (pero hablaremos de eso más adelante). Echemos otro vistazo a esta entrada.
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Aquí tomamos nuestra lista arraysy llamamos a su método sort(), donde pasamos un objeto comparador con un solo método compare()(no nos importa cómo se llame, porque es el único en este objeto, no lo perderemos). Este método toma dos parámetros, con los que trabajaremos a continuación. Si trabajas en IntelliJ IDEA , probablemente hayas visto cómo te ofrece este código para acortar significativamente:
arrays.sort((o1, o2) -> o1.length - o2.length);
Así fue como seis líneas se convirtieron en una corta. Se reescribieron 6 líneas en una corta. Algo ha desaparecido, pero te garantizo que no ha desaparecido nada importante y este código funcionará exactamente igual que con una clase anónima. Tarea 2 . Descubra cómo reescribir la solución al problema 1 usando lambdas (como último recurso, solicite a IntelliJ IDEA que convierta su clase anónima en una lambda).

Hablemos de interfaces

Básicamente, una interfaz es sólo una lista de métodos abstractos. Cuando creamos una clase y decimos que implementará algún tipo de interfaz, debemos escribir en nuestra clase una implementación de los métodos que se enumeran en la interfaz (o, como último recurso, no escribirla, sino hacer que la clase sea abstracta ). Hay interfaces con muchos métodos diferentes (por ejemplo List), hay interfaces con un solo método (por ejemplo, el mismo Comparador o Runnable). Hay interfaces que no tienen ningún método (las llamadas interfaces de marcador, por ejemplo Serializable). Aquellas 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 las interfaces con un solo método las que son adecuadas para su uso con expresiones lambda. Como dije anteriormente, una expresión lambda es un método envuelto en un objeto. Y cuando pasamos un objeto así a alguna parte, de hecho, pasamos este único método. Resulta que no nos importa cómo se llame este método. Lo único que nos importa son los parámetros que toma este método y, de hecho, el código del método en sí. Una expresión lambda es, esencialmente. Implementación de una interfaz funcional. Cuando vemos una interfaz con un método, significa que podemos reescribir dicha clase anónima usando una lambda. Si la interfaz tiene más/menos de un método, entonces la expresión lambda no nos conviene y usaremos una clase anónima, o incluso una normal. Es hora de profundizar en las lambdas. :)

Sintaxis

La sintaxis general es algo como esto:
(параметры) -> {тело метода}
Es decir, paréntesis, dentro de ellos están los parámetros del método, una "flecha" (estos son dos caracteres seguidos: menos y mayor), después de lo cual el cuerpo del método está entre llaves, como siempre. Los parámetros corresponden a los especificados en la interfaz al describir el método. Si el compilador puede definir claramente el tipo de variables (en nuestro caso, se sabe con certeza que estamos trabajando con matrices de cadenas, porque se Listescribe precisamente mediante matrices de cadenas), entonces el tipo de variables String[]no necesita ser escrito.
Si no está seguro, especifique el tipo y IDEA lo resaltará en gris si no es necesario.
Puedes leer más en el tutorial de Oracle , por ejemplo. Esto se llama "escritura de destino" . Puede dar cualquier nombre a las variables, no necesariamente los especificados en la interfaz. Si no hay parámetros, solo paréntesis. Si solo hay un parámetro, solo el nombre de la variable sin paréntesis. Hemos ordenado los parámetros, ahora sobre el cuerpo de la expresión lambda. Dentro de las llaves, escriba el código como para un método normal. Si todo su código consta de una sola línea, no es necesario escribir llaves en absoluto (como ocurre con los if y los bucles). Si su lambda devuelve algo, pero su cuerpo consta de una línea, returnno es necesario escribirlo en absoluto. Pero si tiene llaves, entonces, como en el método habitual, debe escribir explícitamente return.

Ejemplos

Ejemplo 1.
() -> {}
La opción más sencilla. Y el que menos sentido tiene :) Porque no hace nada. Ejemplo 2.
() -> ""
También una opción interesante. No acepta nada y devuelve una cadena vacía ( returnse omite por ser innecesaria). Lo mismo pero con return:
() -> {
    return "";
}
Ejemplo 3. Hola mundo usando lambdas
() -> System.out.println("Hello world!")
No recibe nada, no devuelve nada (no podemos ponerlo returnantes de la llamada System.out.println(), ya que el tipo de retorno en el método println() — void), simplemente muestra una inscripción en la pantalla. Ideal para implementar una interfaz Runnable. El mismo 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 objeto de tipo Runnabley luego pasarla al constructor thread’а:
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 de guardar una expresión lambda en una variable. La interfaz Runnablenos dice que sus objetos deben tener un método public void run(). Según la interfaz, el método de ejecución no acepta nada como parámetro. Y no devuelve nada (void). Por lo tanto al escribir de esta manera se creará un objeto con algún método que no acepta ni devuelve nada. Lo cual es bastante consistente con el método run()en la interfaz Runnable. Es por eso que pudimos poner esta expresión lambda en una variable como Runnable. Ejemplo 4
() -> 42
Nuevamente, no acepta nada, pero devuelve el número 42. Esta expresión lambda se puede colocar en una variable de tipo Callable, porque esta interfaz solo define un método, que se parece a esto:
V call(),
donde Vestá el tipo de valor de retorno (en nuestro caso int). En consecuencia, podemos almacenar dicha expresión lambda de la siguiente manera:
Callable<Integer> c = () -> 42;
Ejemplo 5. Lambda en 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 su tipo de retorno void(ya que no hay return). Ejemplo 6
x -> x
Aquí tomamos algo en una variable хy lo devolvemos. Tenga en cuenta que si solo se acepta un parámetro, no es necesario escribir los paréntesis que lo rodean. Lo mismo, pero entre paréntesis:
(x) -> x
Y aquí está la opción con una explícita return:
x -> {
    return x;
}
O así, entre paréntesis y return:
(x) -> {
    return x;
}
O con una indicación explícita del tipo (y, en consecuencia, entre paréntesis):
(int x) -> x
Ejemplo 7
x -> ++x
Lo aceptamos хy lo devolvemos, pero por 1más. También puedes reescribirlo así:
x -> x + 1
En ambos casos, no indicamos paréntesis alrededor del parámetro, cuerpo del método y palabra return, ya que esto no es necesario. Las opciones entre paréntesis y retorno se describen en el ejemplo 6. Ejemplo 8
(x, y) -> x % y
Aceptamos una parte хy уdevolvemos el resto de la división xpor y. Aquí ya se requieren paréntesis alrededor de los parámetros. Son opcionales sólo cuando hay un solo parámetro. Así con indicación explícita de tipos:
(double x, int y) -> x % y
Ejemplo 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Aceptamos un objeto Cat, una cadena con un nombre y una edad entera. En el método en sí, establecemos el nombre y la edad pasados ​​en Cat. catDado que nuestra variable es un tipo de referencia, el objeto Cat fuera de la expresión lambda cambiará (recibirá el nombre y la edad pasados ​​dentro). Una versión un poco más complicada que utiliza una lambda similar:
public class Main {
    public static void main(String[] args) {
        // crea un gato e imprime en la pantalla para asegurarte de que esté "en blanco"
        Cat myCat = new Cat();
        System.out.println(myCat);

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

        // llamamos al método, al cual le pasamos cat y lambda
        changeEntity(myCat, s);
        // mostrar en pantalla y ver que el estado del gato ha cambiado (tiene nombre y edad)
        System.out.println(myCat);
    }

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

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

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

class Cat implements WithNameAndAge {
    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='Murzik', age=3} Como puede ver, al principio el objeto Cat tenía un estado, pero después de usar la expresión lambda, el estado cambió . Las expresiones lambda funcionan bien con los genéricos. Y si necesitamos crear una clase Dog, por ejemplo, que también implementará WithNameAndAge, entonces en el método main()podemos hacer las mismas operaciones con Dog, sin cambiar la expresión lambda en absoluto. 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 en forma de expresión lambda que devuelva truesi el número pasado es divisible por 13 sin resto.Tarea 4 . Escriba una interfaz funcional con un método que tome dos cadenas y devuelva la misma cadena. Escriba una implementación de dicha interfaz en forma de lambda que devuelva la cadena más larga. Tarea 5 . Escriba una interfaz funcional con un método que acepte tres números fraccionarios: a, by cdevuelva el mismo número fraccionario. Escriba una implementación de dicha interfaz en forma de expresión lambda que devuelva un discriminante. ¿Quién lo olvidó? D = b^2 - 4ac . Tarea 6 . Usando la interfaz funcional de la tarea 5, escriba una expresión lambda que devuelva el resultado de la operación a * b^c. Popular sobre las expresiones lambda en Java. Con ejemplos y tareas. Parte 2.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION