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 2

Publicado en el grupo Random-ES
¿Para quién es este artículo?
  • Para quienes leyeron la primera parte de 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.

Acceso a variables externas

¿Este código se compilará con una clase anónima?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
No. La variable counterdebe ser final. O no necesariamente final, pero en cualquier caso no puede cambiar su valor. El mismo principio se utiliza en las expresiones lambda. Tienen acceso a todas las variables que les son "visibles" desde el lugar donde están declaradas. Pero la lambda no debería cambiarlos (asignar un nuevo valor). Es cierto que existe una opción para evitar esta limitación en clases anónimas. Basta con crear una variable de tipo de referencia y cambiar el estado interno del objeto. En este caso, la variable en sí apuntará al mismo objeto y, en este caso, puedes indicarlo con seguridad como final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Aquí nuestra variable counteres una referencia a un objeto de tipo AtomicInteger. Y para cambiar el estado de este objeto se utiliza el método incrementAndGet(). El valor de la variable en sí no cambia mientras el programa se ejecuta y siempre apunta al mismo objeto, lo que nos permite declarar una variable inmediatamente con la palabra clave final. Los mismos ejemplos, pero con expresiones lambda:
int counter = 0;
Runnable r = () -> counter++;
No compila por la misma razón que la opción con una clase anónima: counterno debería cambiar mientras el programa se está ejecutando. Pero así, todo está bien:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Esto también se aplica a los métodos de llamada. Desde dentro de una expresión lambda, no sólo puede acceder a todas las variables "visibles", sino también llamar a aquellos métodos a los que tiene acceso.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
Aunque el método staticMethod()es privado, es “accesible” para ser llamado dentro del método main(), por lo que también es accesible para llamarlo desde dentro de la lambda que se crea en el método main.

El momento de ejecución del código de expresión lambda.

Esta pregunta puede parecerle demasiado simple, pero vale la pena preguntarse: ¿cuándo se ejecutará el código dentro de la expresión lambda? ¿En el momento de la creación? ¿O en el momento en que (aún se desconoce dónde) se convocará? Es bastante fácil de comprobar.
System.out.println("Запуск программы");

// много всякого разного códigoа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого códigoа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Salida en pantalla:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Se puede ver que el código de la expresión lambda se ejecutó al final, después de que se creó el hilo y solo cuando el proceso de ejecución del programa alcanzó la ejecución real del método run(). Y para nada en el momento de su anuncio. Al declarar una expresión lambda, solo creamos un objeto del tipo Runnabley describimos el comportamiento de su método run(). El método en sí se puso en marcha mucho más tarde.

¿Referencias de métodos?

No está directamente relacionado con las lambdas en sí, pero creo que sería lógico decir algunas palabras al respecto en este artículo. Digamos que tenemos una expresión lambda que no hace nada especial, sino que simplemente llama a algún método.
x -> System.out.println(x)
Le entregaron algo х, y éste simplemente lo llamó System.out.println()y lo pasó por allí х. En este caso, podemos reemplazarlo con un enlace al método que necesitamos. Como esto:
System.out::println
¡Sí, sin los paréntesis al final! Ejemplo más completo:
List<String> strings = new LinkedList<>();
strings.add("Madre");
strings.add("jabón");
strings.add("marco");

strings.forEach(x -> System.out.println(x));
En la última línea usamos un método forEach()que acepta un objeto de interfaz Consumer. Esta es nuevamente una interfaz funcional con un solo método void accept(T t). En consecuencia, escribimos una expresión lambda que toma un parámetro (dado que se escribe en la propia interfaz, no indicamos el tipo de parámetro, pero indicamos que se llamará х). En el cuerpo de la expresión lambda escribimos el código que se ejecutará cuando se llame al método accept(). Aquí simplemente mostramos en la pantalla lo que hay en la variable х. El método en sí forEach()pasa por todos los elementos de la colección, llama Consumeral método del objeto de interfaz que se le pasa (nuestra lambda) accept(), donde pasa cada elemento de la colección. Como ya dije, esta es una expresión lambda (simplemente llamando a otro método) que podemos reemplazar con una referencia al método que necesitamos. Entonces nuestro código se verá así:
List<String> strings = new LinkedList<>();
strings.add("Madre");
strings.add("jabón");
strings.add("marco");

strings.forEach(System.out::println);
Lo principal es que los parámetros aceptados de los métodos (println()y accept()). Dado que el método println()puede aceptar cualquier cosa (está sobrecargado para todas las primitivas y para cualquier objeto), en lugar de una expresión lambda, podemos pasar forEach()solo una referencia al método println(). Luego forEach()tomará cada elemento de la colección y lo pasará directamente a el método println()Para aquellos que se encuentran con esto por primera vez, tenga en cuenta que no llamamos al método System.out.println()(con puntos entre palabras y corchetes al final), sino que pasamos la referencia a este método en sí.
strings.forEach(System.out.println());
tendremos un error de compilación. Porque antes de la llamada forEach(), Java verá que está siendo llamado System.out.println(), entenderá que está siendo devuelto voide intentará voidpasarlo al forEach()objeto de tipo que está esperando allí Consumer.

Sintaxis para usar referencias de métodos

Es bastante simple:
  1. Pasar una referencia a un método estáticoNombreКласса:: NombreСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Madre");
            strings.add("jabón");
            strings.add("marco");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. Pasar una referencia a un método no estático usando un objeto existenteNombreПеременнойСОбъектом:: nombre del método

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Madre");
            strings.add("jabón");
            strings.add("marco");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. Pasamos una referencia a un método no estático usando la clase en la que se implementa dicho método.NombreКласса:: nombre del método

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            users.forEach(User::print);
        }
    
        private static class User {
            private String name;
    
            private User(String name) {
                this.name = name;
            }
    
            private void print() {
                System.out.println(name);
            }
        }
    }
  4. Pasar un enlace al constructor NombreКласса::new
    Usar enlaces de métodos es muy conveniente cuando hay un método listo para usar con el que está completamente satisfecho y le gustaría usarlo como una devolución de llamada. En este caso, en lugar de escribir una expresión lambda con el código de ese método, o una expresión lambda donde simplemente llamamos a este método, simplemente le pasamos una referencia. Eso es todo.

Interesante diferencia entre clase anónima y expresión lambda

En una clase anónima, la palabra clave thisapunta a un objeto de esa clase anónima. Y si lo usamos thisdentro de una lambda, obtendremos acceso al objeto de la clase de marco. Donde realmente escribimos esta expresión. Esto sucede porque las expresiones lambda, cuando se compilan, se convierten en un método privado de la clase donde están escritas. No recomendaría utilizar esta "función", ya que tiene un efecto secundario que contradice los principios de la programación funcional. Pero este enfoque es bastante consistente con la programación orientada a objetos. ;)

¿De dónde saqué la información o qué más leer?

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION