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 2

Publicado en el grupo Random-ES
¿Para quién es este artículo?
  • Es para las personas que leyeron la primera parte de 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.
Si 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. Una explicación de las expresiones lambda en Java.  Con ejemplos y tareas.  parte 2 - 1El 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.

Acceso a variables externas

¿Este código se compila con una clase anónima?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
No. La counter variable debe ser final. O si no final, al menos no puede cambiar su valor. El mismo principio se aplica en las expresiones lambda. Pueden acceder a todas las variables que pueden "ver" desde el lugar donde se declaran. Pero una lambda no debe cambiarlos (asignarles un nuevo valor). Sin embargo, hay una forma de eludir esta restricción en las clases anónimas. Simplemente cree una variable de referencia y cambie el estado interno del objeto. Al hacerlo, la variable en sí no cambia (apunta al mismo objeto) y se puede marcar con seguridad como final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Aquí nuestra countervariable es una referencia a un AtomicIntegerobjeto. Y el incrementAndGet()método se usa para cambiar el estado de este objeto. El valor de la variable en sí no cambia mientras se ejecuta el programa. Siempre apunta al mismo objeto, lo que nos permite declarar la variable con la palabra clave final. Estos son los mismos ejemplos, pero con expresiones lambda:

int counter = 0;
Runnable r = () -> counter++;
Esto no se compilará por la misma razón que la versión con una clase anónima:  counterno debe cambiar mientras se ejecuta el programa. Pero todo está bien si lo hacemos así:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Esto también se aplica a los métodos de llamada. Dentro de las expresiones lambda, no solo puede acceder a todas las variables "visibles", sino también llamar a cualquier método accesible.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Aunque staticMethod()es privado, es accesible dentro del main()método, por lo que también se puede llamar desde dentro de una lambda creada en el mainmétodo.

¿Cuándo se ejecuta una expresión lambda?

Puede encontrar la siguiente pregunta demasiado simple, pero debe formularla de la misma manera: ¿cuándo se ejecutará el código dentro de la expresión lambda? ¿Cuándo se crea? ¿O cuándo se llama (que aún no se sabe)? Esto es bastante fácil de comprobar.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Salida de pantalla:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Puede ver que la expresión lambda se ejecutó al final, después de que se creó el subproceso y solo cuando la ejecución del programa llega al run()método. Ciertamente no cuando se declara. Al declarar una expresión lambda, solo creamos un Runnableobjeto y describimos cómo run()se comporta su método. El método en sí se ejecuta mucho más tarde.

¿Referencias de métodos?

Las referencias a métodos no están directamente relacionadas con las lambdas, pero creo que tiene sentido decir algunas palabras sobre ellas en este artículo. Supongamos que tenemos una expresión lambda que no hace nada especial, sino que simplemente llama a un método.

x -> System.out.println(x)
Recibe algunas xy solo llamadas System.out.println(), de paso x. En este caso, podemos reemplazarlo con una referencia al método deseado. Como esto:

System.out::println
Así es, ¡sin paréntesis al final! Aquí hay un ejemplo más completo:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
En la última línea, usamos el forEach()método, que toma un objeto que implementa la Consumerinterfaz. Nuevamente, esta es una interfaz funcional, que tiene solo un void accept(T t)método. En consecuencia, escribimos una expresión lambda que tiene un parámetro (debido a que se escribe en la propia interfaz, no especificamos el tipo de parámetro; solo indicamos que lo llamaremos x). En el cuerpo de la expresión lambda, escribimos el código que se ejecutará cuando accept()se llame al método. Aquí simplemente mostramos lo que terminó en la xvariable. Este mismo forEach()método itera a través de todos los elementos de la colección y llama al accept()método en la implementación delConsumerinterfaz (nuestra lambda), pasando cada elemento de la colección. Como dije, podemos reemplazar una expresión lambda de este tipo (una que simplemente clasifique un método diferente) con una referencia al método deseado. Entonces nuestro código se verá así:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
Lo principal es que los parámetros de los métodos println()y accept()coincidan. Debido a que el println()método puede aceptar cualquier cosa (está sobrecargado para todos los tipos primitivos y todos los objetos), en lugar de expresiones lambda, simplemente podemos pasar una referencia al println()método a forEach(). Luego forEach()tomará cada elemento de la colección y lo pasará directamente al println()método. Para cualquiera que se encuentre con esto por primera vez, tenga en cuenta que no estamos llamando System.out.println()(con puntos entre palabras y paréntesis al final). En cambio, estamos pasando una referencia a este método. Si escribimos esto

strings.forEach(System.out.println());
tendremos un error de compilación. Antes de la llamada a forEach(), Java ve que System.out.println()se está llamando, por lo que entiende que el valor de retorno es voide intentará pasar voida forEach(), que en cambio espera un Consumerobjeto.

Sintaxis para referencias de métodos

Es bastante simple:
  1. Pasamos una referencia a un método estático como este:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. Pasamos una referencia a un método no estático utilizando un objeto existente, como este:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            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 que lo implementa de la siguiente manera:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            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. Pasamos una referencia a un constructor como este:ClassName::new

    Las referencias de métodos son muy convenientes cuando ya tiene un método que funcionaría perfectamente como una devolución de llamada. En este caso, en lugar de escribir una expresión lambda que contenga el código del método, o escribir una expresión lambda que simplemente llame al método, simplemente le pasaremos una referencia. Y eso es.

Una distinción interesante entre clases anónimas y expresiones lambda

En una clase anónima, la thispalabra clave apunta a un objeto de la clase anónima. Pero si usamos esto dentro de una lambda, obtenemos acceso al objeto de la clase contenedora. En el que escribimos la expresión lambda. Esto sucede porque las expresiones lambda se compilan en un método privado de la clase en la que están escritas. No recomendaría usar esta "característica", ya que tiene un efecto secundario y contradice los principios de la programación funcional. Dicho esto, este enfoque es totalmente consistente con OOP. ;)

¿De dónde obtuve mi información y qué más debe leer?

Y, por supuesto, encontré un montón de cosas en Google :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION