CodeGym /Cursos /JAVA 25 SELF /Expresiones lambda: sintaxis y ámbitos de visibilidad

Expresiones lambda: sintaxis y ámbitos de visibilidad

JAVA 25 SELF
Nivel 48 , Lección 0
Disponible

1. Introducción

Seamos honestos: escribir clases anónimas por una o dos líneas de código es como contratar un camión enorme para transportar un solo panecillo de la panadería a la tienda.

Por ejemplo, si quieres ordenar una lista de cadenas por longitud, antes de Java 8 había que escribir así:

List<String> list = Arrays.asList("manzana", "plátano", "kiwi");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

La tarea parece simple, pero el código ocupa media pantalla. Molesta especialmente cuando hay muchas operaciones de este tipo: el código se vuelve «ruidoso» y se pierde la esencia del problema. Los programadores lloraban, sufrían y luego inventaron las expresiones lambda. Ya las hemos estudiado un poco; ahora vamos a repasarlas y profundizar en nuestros conocimientos.

Qué es una expresión lambda

Una expresión lambda es una forma compacta de escribir la implementación de una interfaz funcional, es decir, una interfaz con un único método abstracto (por ejemplo, Comparator, Runnable, Consumer y muchas otras).

Dicho de forma sencilla, una expresión lambda te permite escribir una función «al vuelo», justo donde la necesitas, sin declarar una clase o un método por separado.

Sintaxis general:

(parámetros) -> { cuerpo }

Ejemplos:

  • (a, b) -> a + b — función que suma dos números
  • x -> x * x — función que eleva un número al cuadrado
  • () -> System.out.println("Hello!") — función sin parámetros

Relación con las interfaces funcionales:
Una expresión lambda siempre puede asignarse a una variable de tipo interfaz funcional o pasarse como argumento a un método que espera dicha interfaz.

2. Sintaxis de las expresiones lambda

Sin parámetros

Runnable r = () -> System.out.println("¡Hola, mundo!");
r.run(); // Mostrará: ¡Hola, mundo!

Un parámetro
Si solo hay un parámetro, se pueden omitir los paréntesis:

Consumer<String> print = s -> System.out.println(s);
print.accept("¡Java es genial!");

Varios parámetros
Los paréntesis son obligatorios:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

Cuerpo con una sola expresión
Si el cuerpo consta de una sola expresión, no hacen falta llaves ni return:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

Cuerpo en bloque
Si necesitas varias instrucciones, usa llaves y return (si hay valor de retorno):

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

Tipos de parámetros
La mayoría de las veces no es necesario indicar los tipos de los parámetros: el compilador los infiere por el contexto. Pero si quieres, puedes especificarlos explícitamente:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

Lambda sin valor de retorno
Si la interfaz retorna void, simplemente escribe las instrucciones:

list.forEach(s -> System.out.println("Elemento: " + s));

Tabla: variantes de sintaxis de expresiones lambda

Qué hacemos Ejemplo Comentario
Sin parámetros
() -> System.out.println("Hi")
Por ejemplo, para Runnable
Un parámetro
x -> x * x
Se pueden omitir los paréntesis
Varios parámetros
(a, b) -> a + b
Los paréntesis son obligatorios
Una sola expresión
x -> x + 1
Sin return ni llaves
Bloque de código
x -> { int y = x + 1; return y * 2; }
Con return si hay resultado

3. Aplicación: dónde y cómo usar expresiones lambda

Las expresiones lambda se usan sobre todo donde se necesita pasar un «comportamiento», es decir, una función, como argumento. Esto supuso una verdadera revolución para las colecciones, los flujos (Stream API), los eventos y mucho más.

Ordenación de una lista

Antes de Java 8:

list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Con una expresión lambda:

list.sort((a, b) -> a.length() - b.length());

Creación de un hilo (Thread)

Thread t = new Thread(() -> System.out.println("¡Hilo iniciado!"));
t.start();

Procesamiento de colecciones

list.forEach(s -> System.out.println(s.toUpperCase()));

Filtrado de una lista

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

Ejemplo: desarrollamos nuestra aplicación de aprendizaje

Supongamos que tenemos una lista de usuarios:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

Mostrar todos los usuarios cuyos nombres tengan más de 4 caracteres:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("Usuario: " + name));

4. Ámbito de las variables en expresiones lambda

Las expresiones lambda pueden usar variables del método circundante, pero hay matices.

Variables y lambdas

Una lambda en Java puede «capturar» solo aquellas variables que no cambian después de su inicialización. Si la variable se declara con final, todo es evidente. Pero incluso si la palabra final no está escrita, el compilador comprueba por sí mismo si el valor cambia o no. Si no cambia, la considera «como si fuera final» y permite usarla en la lambda sin problemas.

Ejemplo:

int minLength = 4; // el valor no cambia en ninguna parte
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

Aquí todo funciona porque minLength sigue siendo el mismo número.

Pero si después de usarla en la lambda intentas reasignar minLength, obtendrás un error de compilación:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // ¡Error! La lambda ya capturó el valor

En esencia, la regla es muy simple: la variable que entra en una lambda debe ser inmutable.

Diferencia con las clases anónimas

En las clases anónimas y en las expresiones lambda, las variables del método externo funcionan igual: solo final/efectivamente final.

¡PERO!
En una expresión lambda, this se refiere al objeto externo (la instancia actual de la clase), mientras que en una clase anónima se refiere a la instancia de la propia clase anónima. Esto es importante si dentro de la lambda accedes a campos o métodos de la clase actual.

Ejemplo:

public class Example {
    String name = "Clase externa";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "Clase anónima";
            @Override
            public void run() {
                System.out.println(this.name); // "Clase anónima"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "Clase externa"

        r1.run();
        r2.run();
    }
}

5. Errores típicos al trabajar con expresiones lambda

Error n.º 1: usar una variable que no es final/efectivamente final. Si decides cambiar una variable después de haberla usado en una lambda, el compilador te detendrá de inmediato. Esto se hace por seguridad: de lo contrario, no estaría claro qué valor de la variable utilizar.

Error n.º 2: confusión con this. En una expresión lambda, this es la clase externa, y en una clase anónima es la propia clase anónima. Si intentas llamar a un método de la clase externa desde una lambda, todo funcionará; en cambio, desde una clase anónima no (si contabas con el contexto de la clase externa).

Error n.º 3: lambda sin contexto. No se puede usar una expresión lambda por sí sola: hay que asignarla a una variable de una interfaz funcional o pasarla allí donde se espera esa interfaz. Intentar escribir simplemente x -> x + 1 fuera de contexto provocará un error.

Error n.º 4: lambda demasiado compleja. Si la expresión lambda supera las 3–5 líneas, se vuelve difícil de leer. En esos casos, es mejor extraer la lógica a un método aparte.

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