CodeGym /Cursos /JAVA 25 SELF /Introducción a las expresiones lambda

Introducción a las expresiones lambda

JAVA 25 SELF
Nivel 21, Lección 0
Disponible

1. Primeros pasos

En pocas palabras: una expresión lambda es una forma de crear rápidamente una implementación de una interfaz funcional «al vuelo», sin declarar una clase aparte ni una clase anónima. Es como un mini-método sin nombre que se puede pasar como argumento o guardar en una variable.

Durante mucho tiempo, Java fue un lenguaje POO «clásico». Si querías pasar un trozo de comportamiento, por ejemplo, qué hacer al pulsar un botón, había que escribir clases anónimas:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("¡Botón pulsado!");
    }
});

Con Java 8 llegó la magia de las lambdas:

button.setOnClickListener(() -> System.out.println("¡Botón pulsado!"));

Donde () -> System.out.println("¡Botón pulsado!") es precisamente una expresión lambda.

Formalmente: una expresión lambda es una forma compacta de escribir la implementación del único método abstracto de una interfaz funcional.

¿Por qué es importante?

  • Permitir pasar comportamiento como un valor (por ejemplo, qué hacer con cada elemento de una lista).
  • Escribir código compacto y legible.
  • Usar las API modernas de Java: Stream API, manejo de eventos, tareas asíncronas y mucho más.

2. Sintaxis de las expresiones lambda

Forma general

(parámetros) -> expresión
// o
(parámetros) -> { bloque de código }

Ejemplos para distintas interfaces

1. Sin parámetros (por ejemplo, Runnable):

Runnable r = () -> System.out.println("¡Hola, lambda!");
r.run();

2. Un parámetro (por ejemplo, Consumer):

Consumer<String> printer = s -> System.out.println("Has pasado: " + s);
printer.accept("Java");

Si el parámetro es único y su tipo se puede inferir, se pueden omitir los paréntesis.
El tipo String es el tipo de la variable (s) que pasamos allí.

3. Varios parámetros (por ejemplo, Comparator):

Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5

El tipo Integer es el tipo de las variables (a, b) que pasamos.

4. Cuerpo multilínea (se necesitan llaves y return si hay retorno explícito):

Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(6)); // 36

El primer Integer es el tipo del resultado de la función; el segundo Integer es el tipo del parámetro.

Atajos y concisión

  • Si el cuerpo es una sola expresión, se pueden omitir las llaves y return.
  • Si no hay parámetros, se escriben paréntesis vacíos: () -> ...
  • Si hay un solo parámetro, se pueden omitir los paréntesis: x -> ...
  • Si hay más de un parámetro, se necesitan paréntesis: (a, b) -> ...

Tabla resumen

Veamos cómo se expresa la misma idea con una lambda y con una clase anónima:

Interfaz Expresión lambda (ejemplo) Equivalente con clase anónima
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

Ejemplo con una interfaz propia

Supongamos que tenemos una interfaz funcional:

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

Antes se implementaba así:

Operation sum = new Operation() {
    @Override
    public void apply(int a, int b) {
        return a + b;
    }
};

Ahora — a la moderna:

Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8

Ejemplos para interfaces estándar

Runnable:

Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();

Comparator:

List<String> list = Arrays.asList("manzana", "plátano", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);

Function:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123

3. Ámbito y captura de variables

Variables del contexto externo (effectively final)

Las expresiones lambda pueden utilizar variables del método circundante. Pero hay una regla: dichas variables deben ser effectively final, es decir, o bien declaradas explícitamente como final, o bien no modificarse después de su inicialización.

Ejemplo:

String prefix = "Resultado: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix aquí está “congelado”: después de esto no se puede cambiar
System.out.println(f.apply(5)); // Resultado: 10

Si intentas modificar prefix después de que se haya usado en la lambda, el compilador dará un error.

¿Por qué?
Una expresión lambda puede invocarse después de salir del método en el que se declaró la variable. Para evitar errores «mágicos», Java permite utilizar solo variables inmutables.

Diferencia con las clases anónimas

En las clases anónimas rige la misma regla: las variables del método externo deben ser final/effectively final. Pero hay matices con el ámbito: dentro de una clase anónima, this hace referencia a la propia clase anónima, mientras que en una lambda hace referencia a la clase externa.

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // Imprimirá: Demo$1 (clase anónima)
            }
        };

        Runnable r2 = () -> System.out.println(this); // Imprimirá: Demo (clase externa)

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

Lambdas y campos de clase

Una expresión lambda puede acceder a los campos de la clase externa sin restricciones:

public class Counter {
    private int base = 10;

    public void printSum(int x) {
        Function<Integer, Integer> sum = y -> base + y + x;
        System.out.println(sum.apply(5));
    }
}

Aquí base se puede modificar (es un campo de clase).
x debe ser effectively final.

4. Práctica: escribamos algunas expresiones lambda

Ejemplo: filtrado de una lista de números

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("Par: " + n));

Aprenderás más sobre la Stream API en el nivel 30 :P

Ejemplo: función de transformación de cadena

Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA

Ejemplo: interfaz funcional propia

@FunctionalInterface
interface StringTransformer {
    String transform(String s);
}

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Hola")); // ¡Hola!

Ejemplo: uso de variables del contexto externo

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// después de esto no se puede modificar factor

5. Errores típicos al trabajar con expresiones lambda

Error n.º 1: intentar modificar una variable capturada por la lambda.
Este código no compilará — la variable debe ser effectively final:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Error: ¡sum no es final!

Si necesitas acumular valores, usa un array o un objeto envoltorio.

Error n.º 2: confusión con el ámbito de this.
Dentro de una expresión lambda, this se refiere a la clase externa, no a la lambda (a diferencia de una clase anónima).

Error n.º 3: olvidar las llaves y return en una lambda multilínea.
Si el cuerpo de la lambda no es una sola expresión, se necesitan llaves y return:

Function<Integer, Integer> square = x -> {
    int y = x * x;
    return y;
};

Error n.º 4: determinar incorrectamente el tipo de la expresión lambda.
Una expresión lambda siempre implementa una interfaz funcional. No puedes simplemente escribir:

var f = x -> x + 1; // ¡Error! Se desconoce qué tipo de interfaz es.

Hay que indicar el tipo explícitamente:

Function<Integer, Integer> f = x -> x + 1;
1
Tarea
JAVA 25 SELF, nivel 21, lección 0
Bloqueada
Anuncio de voz del sistema 📣
Anuncio de voz del sistema 📣
1
Tarea
JAVA 25 SELF, nivel 21, lección 0
Bloqueada
Estandarización de la entrada del usuario 📝
Estandarización de la entrada del usuario 📝
1
Tarea
JAVA 25 SELF, nivel 21, lección 0
Bloqueada
Clasificación de deportistas por "fuerza del golpe" 🥊
Clasificación de deportistas por "fuerza del golpe" 🥊
1
Tarea
JAVA 25 SELF, nivel 21, lección 0
Bloqueada
Generación dinámica de informes 📊
Generación dinámica de informes 📊
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION