Java

Tutorial Java: Funciones lambda

Aprende la sintaxis, uso y scope de funciones lambda en Java para programación funcional y manejo eficiente de colecciones y concurrencia.

Aprende Java y certifícate

Sintaxis y estructura básica de expresiones lambda

Las expresiones lambda representan una de las características más importantes introducidas en Java 8, permitiendo implementar el paradigma de programación funcional. Una expresión lambda es, esencialmente, un bloque de código que puede ser pasado como argumento a métodos o almacenado en variables, similar a lo que podríamos considerar una función anónima.

La sintaxis básica de una expresión lambda en Java sigue este patrón:

(parámetros) -> expresión

o para bloques de código más extensos:

(parámetros) -> {
    // Cuerpo con múltiples instrucciones
    return resultado;
}

Componentes de una expresión lambda

Una expresión lambda consta de tres partes principales:

  • Lista de parámetros: Encerrados entre paréntesis y separados por comas
  • Operador flecha (->)
  • Cuerpo de la lambda: Una expresión o un bloque de código entre llaves

Variaciones en la sintaxis

La sintaxis de las expresiones lambda puede variar según diferentes situaciones:

  • Sin parámetros:
() -> System.out.println("Lambda sin parámetros")
  • Un solo parámetro (los paréntesis son opcionales):
x -> x * x

o con paréntesis:

(x) -> x * x
  • Múltiples parámetros:
(x, y) -> x + y
  • Con tipos de datos explícitos:
(int x, int y) -> x + y
  • Expresión de una sola línea (sin llaves ni return):
(String s) -> s.length()
  • Bloque de código con múltiples líneas:
(String s) -> {
    int length = s.length();
    return length > 5;
}

Ejemplos prácticos

Veamos algunos ejemplos prácticos de expresiones lambda en diferentes contextos:

  • Ordenar una lista de cadenas por longitud:
List<String> nombres = Arrays.asList("Ana", "Juan", "Alejandro", "Eva");

// Antes de Java 8 (clase anónima)
Collections.sort(nombres, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

// Con expresión lambda
Collections.sort(nombres, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
  • Filtrar elementos de una colección:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> pares = new ArrayList<>();

// Filtrar números pares
numeros.forEach(n -> {
    if (n % 2 == 0) {
        pares.add(n);
    }
});
  • Ejecutar código en un hilo:
// Antes de Java 8
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Ejecutando en otro hilo");
    }
}).start();

// Con expresión lambda
new Thread(() -> System.out.println("Ejecutando en otro hilo con lambda")).start();

Reglas de sintaxis importantes

Al trabajar con expresiones lambda, es importante tener en cuenta estas reglas:

  • Inferencia de tipos: Java puede inferir los tipos de los parámetros basándose en el contexto.
// El compilador infiere que 'n' es un Integer
List<Integer> numeros = Arrays.asList(1, 2, 3);
numeros.forEach(n -> System.out.println(n));
  • Return implícito: En expresiones de una sola línea sin llaves, el valor se retorna automáticamente.
// Estas dos expresiones son equivalentes
Function<Integer, Integer> cuadrado1 = n -> n * n;
Function<Integer, Integer> cuadrado2 = n -> { return n * n; };
  • Paréntesis obligatorios: Si no hay parámetros o hay más de uno, los paréntesis son obligatorios.
Runnable tarea = () -> System.out.println("Tarea sin parámetros");
BiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;
  • Llaves obligatorias: Si el cuerpo tiene más de una instrucción, las llaves son obligatorias.
Consumer<String> procesador = s -> {
    String resultado = s.toUpperCase();
    System.out.println(resultado);
    // Más instrucciones...
};

Limitaciones de las expresiones lambda

Las expresiones lambda tienen algunas limitaciones en su sintaxis:

  • No pueden declarar variables con el mismo nombre que variables en el ámbito externo.
  • No pueden usar la palabra clave this para referirse a la instancia de la lambda (se refiere a la clase contenedora).
  • No pueden usar break o continue para salir de la lambda.
// Esto NO compila
int contador = 0;
Runnable r = () -> {
    int contador = 1; // Error: Variable 'contador' ya está definida
    System.out.println(contador);
};

Las expresiones lambda proporcionan una forma concisa y expresiva de implementar interfaces funcionales, lo que permite escribir código más limpio y legible, especialmente cuando se trabaja con colecciones y operaciones de procesamiento de datos.

Contextos de uso y target typing

Las expresiones lambda en Java no existen de forma aislada, sino que siempre se utilizan en un contexto específico. A diferencia de lenguajes como JavaScript o Python, donde las funciones son ciudadanos de primera clase, en Java las lambdas están vinculadas al concepto de interfaces funcionales.

El compilador de Java utiliza un mecanismo llamado target typing (o tipado objetivo) para determinar qué interfaz funcional está implementando una expresión lambda. Este mecanismo permite que el compilador infiera los tipos de los parámetros y el tipo de retorno basándose en el contexto donde se utiliza la lambda.

Interfaces funcionales como objetivos

Para que una expresión lambda pueda ser utilizada, debe existir un "objetivo" que sea una interfaz funcional - una interfaz que contiene exactamente un método abstracto. Algunos ejemplos comunes incluyen:

// Contextos comunes donde se utilizan lambdas
Runnable tarea = () -> System.out.println("Ejecutando tarea");
Comparator<String> comparador = (s1, s2) -> s1.length() - s2.length();
ActionListener listener = e -> System.out.println("Botón pulsado");

En cada caso, el compilador determina qué interfaz funcional está implementando la lambda basándose en el contexto de asignación.

Asignación a variables

El contexto más directo es la asignación a una variable cuyo tipo es una interfaz funcional:

// El tipo de la variable define el "target type"
Predicate<String> estaVacio = s -> s.isEmpty();
Consumer<Integer> mostrar = n -> System.out.println(n);
Supplier<Double> numeroAleatorio = () -> Math.random();

En estos ejemplos, el compilador sabe exactamente qué método abstracto debe implementar la lambda porque está definido por el tipo de la variable.

Parámetros de métodos

Otro contexto común es cuando una lambda se pasa como argumento a un método:

List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz");

// La lambda implementa Consumer<String> porque forEach espera ese tipo
nombres.forEach(nombre -> System.out.println(nombre));

// La lambda implementa Predicate<String> porque removeIf espera ese tipo
nombres.removeIf(nombre -> nombre.length() > 5);

El compilador determina el tipo de la lambda basándose en la firma del método al que se pasa como argumento.

Valores de retorno

Las lambdas también pueden ser valores de retorno de métodos:

public Comparator<String> obtenerComparadorPorLongitud() {
    // Retorna una lambda que implementa Comparator<String>
    return (s1, s2) -> Integer.compare(s1.length(), s2.length());
}

Target typing en expresiones

El target typing también funciona en expresiones más complejas:

// En una expresión condicional
Predicate<String> filtro = condicion 
    ? s -> s.startsWith("A") 
    : s -> s.endsWith("Z");

// En constructores
new Thread(() -> System.out.println("Nuevo hilo")).start();

Inferencia de tipos

Gracias al target typing, el compilador puede inferir los tipos de los parámetros de la lambda:

// El compilador infiere que 'n' es Integer
List<Integer> numeros = Arrays.asList(1, 2, 3);
numeros.forEach(n -> System.out.println(n * n));

// Comparación con tipos explícitos
numeros.forEach((Integer n) -> System.out.println(n * n));

Ambas versiones son equivalentes, pero la primera es más concisa gracias a la inferencia de tipos.

Ambigüedad y resolución de tipos

En algunos casos, puede haber ambigüedad en el target type:

// Método sobrecargado
public void procesar(Consumer<String> consumidor) { /* ... */ }
public void procesar(Predicate<String> predicado) { /* ... */ }

// ¿Qué método se llama? ¡Ambigüedad!
procesar(s -> System.out.println(s)); // Error de compilación

En estos casos, es necesario proporcionar información adicional para resolver la ambigüedad:

// Soluciones a la ambigüedad
procesar((Consumer<String>) s -> System.out.println(s));
// O usar una referencia de método
procesar(System.out::println);

Contextos prácticos de uso

Las expresiones lambda se utilizan en diversos contextos prácticos:

  • Operaciones con colecciones:
List<String> nombres = Arrays.asList("Ana", "Juan", "Carlos");
// Filtrado
List<String> nombresFiltrados = nombres.stream()
    .filter(nombre -> nombre.length() > 3)
    .collect(Collectors.toList());
  • Eventos en interfaces gráficas:
JButton boton = new JButton("Pulsar");
boton.addActionListener(e -> System.out.println("Botón pulsado"));
  • Programación concurrente:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    // Código a ejecutar en otro hilo
    return "Tarea completada";
});
  • Operaciones de ordenación:
List<Persona> personas = obtenerPersonas();
// Ordenar por edad
Collections.sort(personas, (p1, p2) -> Integer.compare(p1.getEdad(), p2.getEdad()));

Conversión de contexto

El contexto de conversión es otro aspecto importante del target typing:

// Método que acepta un Runnable
void ejecutarEnHilo(Runnable tarea) {
    new Thread(tarea).start();
}

// Llamada al método con una lambda
ejecutarEnHilo(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("Iteración " + i);
    }
});

En este ejemplo, la lambda se convierte automáticamente al tipo Runnable porque ese es el tipo esperado por el método.

El target typing es un mecanismo fundamental que permite que las expresiones lambda en Java sean concisas y expresivas, al tiempo que mantiene la seguridad de tipos característica del lenguaje. Entender cómo funciona este mecanismo es esencial para utilizar lambdas de manera efectiva en diferentes contextos.

Variables capturadas y scope

Las expresiones lambda en Java no solo pueden utilizar parámetros que reciben, sino que también pueden acceder a variables definidas en el contexto donde se declaran. Esta capacidad de "capturar" variables del entorno circundante es una característica fundamental que amplía significativamente la utilidad de las lambdas.

Captura de variables

Cuando una expresión lambda utiliza variables definidas fuera de su cuerpo, se dice que captura esas variables. Existen dos tipos de variables que pueden ser capturadas:

  • Variables de instancia y estáticas: Accesibles sin restricciones
  • Variables locales: Deben ser efectivamente finales
class EjemploCaptura {
    // Variable de instancia
    private int contador = 0;
    
    public void incrementarContador() {
        // La lambda captura la variable de instancia 'contador'
        Runnable incrementador = () -> {
            contador++; // Acceso permitido a variable de instancia
            System.out.println("Contador: " + contador);
        };
        
        new Thread(incrementador).start();
    }
}

Restricción de efectivamente final

Una característica importante de las lambdas en Java es la restricción de que las variables locales capturadas deben ser efectivamente finales. Una variable es efectivamente final cuando su valor no cambia después de su inicialización, aunque no esté explícitamente declarada como final.

public void procesarDatos() {
    String prefijo = "ID-"; // Variable local efectivamente final
    
    // Correcto: la variable 'prefijo' no se modifica después de su inicialización
    Consumer<String> procesador = dato -> System.out.println(prefijo + dato);
    
    procesarItems(procesador);
    
    // Si intentáramos esto, obtendríamos un error de compilación:
    // prefijo = "CÓDIGO-"; // Error: Variable used in lambda should be final or effectively final
}

Esta restricción existe porque:

  1. Las lambdas pueden ejecutarse en contextos asíncronos (como hilos separados)
  2. Las variables locales se almacenan en la pila y podrían dejar de existir cuando el método termine
  3. Para evitar condiciones de carrera y comportamientos impredecibles

Diferencia con variables de instancia

A diferencia de las variables locales, las variables de instancia y estáticas pueden modificarse dentro de las lambdas sin restricciones:

class Contador {
    private int valor = 0; // Variable de instancia
    
    public void incrementar() {
        // Podemos modificar la variable de instancia 'valor' dentro de la lambda
        Runnable tarea = () -> {
            for (int i = 0; i < 10; i++) {
                valor++; // Permitido: modificación de variable de instancia
            }
            System.out.println("Valor final: " + valor);
        };
        
        new Thread(tarea).start();
    }
}

Esto es posible porque las variables de instancia se almacenan en el heap (montículo) y no en la pila, por lo que siguen existiendo mientras el objeto exista.

Soluciones para variables mutables

Cuando necesitamos modificar un contador o acumulador dentro de una lambda, podemos usar estas alternativas:

  • Usar variables de instancia como vimos anteriormente
  • Utilizar clases contenedoras como AtomicInteger o arrays de un solo elemento
public void contarElementos(List<String> elementos) {
    // Usando un array de un solo elemento como contenedor mutable
    final int[] contador = {0};
    
    elementos.forEach(elemento -> {
        if (elemento.length() > 5) {
            contador[0]++; // Modificamos el contenido del array, no la referencia
        }
    });
    
    System.out.println("Elementos largos: " + contador[0]);
}
  • Usar clases atómicas para operaciones thread-safe:
public void procesarConcurrentemente(List<String> datos) {
    AtomicInteger contador = new AtomicInteger(0);
    
    datos.parallelStream().forEach(dato -> {
        // Procesamiento del dato
        contador.incrementAndGet(); // Incremento atómico y thread-safe
    });
    
    System.out.println("Elementos procesados: " + contador.get());
}

Scope de las variables en lambdas

El ámbito o scope de las variables dentro de una lambda sigue reglas específicas:

  • Las lambdas crean su propio ámbito para variables locales
  • No pueden redeclarar variables que existen en el ámbito contenedor
  • Pueden ocultar variables de ámbitos externos si declaran nuevas con el mismo nombre
public void ejemploScope() {
    String mensaje = "Mensaje original";
    
    // Correcto: la lambda captura 'mensaje' del ámbito externo
    Runnable r1 = () -> System.out.println(mensaje);
    
    // Error: no se puede redeclarar 'mensaje' como parámetro
    // Consumer<String> c = mensaje -> System.out.println(mensaje);
    
    // Correcto: la lambda declara su propia variable local 'i'
    for (int i = 0; i < 5; i++) {
        int valorFinal = i; // Variable efectivamente final para captura
        Runnable r2 = () -> {
            // int i = 10; // Error: no se puede redeclarar 'i'
            System.out.println("Valor capturado: " + valorFinal);
        };
        r2.run();
    }
}

La palabra clave 'this' en lambdas

El comportamiento de la palabra clave this dentro de las lambdas es diferente al de las clases anónimas:

  • En una lambda, this se refiere a la instancia de la clase contenedora
  • En una clase anónima, this se refiere a la instancia de la clase anónima
class DemostradorThis {
    private String nombre = "Instancia contenedora";
    
    public void mostrarThis() {
        // En la lambda, 'this' se refiere a la instancia de DemostradorThis
        Runnable lambda = () -> {
            System.out.println("En lambda, this.nombre = " + this.nombre);
        };
        
        // En la clase anónima, 'this' se refiere a la instancia de Runnable
        Runnable anonima = new Runnable() {
            private String nombre = "Clase anónima";
            
            @Override
            public void run() {
                System.out.println("En anónima, this.nombre = " + this.nombre);
                // Para acceder a la instancia externa:
                System.out.println("Instancia externa: " + DemostradorThis.this.nombre);
            }
        };
        
        lambda.run();   // Imprime: "En lambda, this.nombre = Instancia contenedora"
        anonima.run();  // Imprime: "En anónima, this.nombre = Clase anónima"
    }
}

Esta diferencia hace que las lambdas sean más intuitivas cuando se trabaja con el contexto de la clase contenedora.

Ejemplos prácticos de captura de variables

Veamos algunos ejemplos prácticos de cómo se utilizan las variables capturadas:

  • Configuración de comportamiento:
public List<String> filtrarPorPrefijo(List<String> elementos, String prefijo) {
    // La lambda captura la variable 'prefijo'
    return elementos.stream()
        .filter(elemento -> elemento.startsWith(prefijo))
        .collect(Collectors.toList());
}
  • Acumulación de resultados:
public Map<String, Integer> contarPalabras(List<String> textos) {
    Map<String, Integer> frecuencias = new HashMap<>();
    
    // La lambda captura la variable 'frecuencias'
    textos.forEach(texto -> {
        String[] palabras = texto.split("\\s+");
        for (String palabra : palabras) {
            frecuencias.merge(palabra.toLowerCase(), 1, Integer::sum);
        }
    });
    
    return frecuencias;
}
  • Personalización de comportamiento:
public void procesarConTimeout(Runnable tarea, long timeoutMs) {
    // La lambda captura las variables 'tarea' y 'timeoutMs'
    Thread hilo = new Thread(() -> {
        long inicio = System.currentTimeMillis();
        tarea.run();
        long duracion = System.currentTimeMillis() - inicio;
        
        if (duracion > timeoutMs) {
            System.out.println("Advertencia: la tarea excedió el timeout de " + timeoutMs + "ms");
        }
    });
    
    hilo.start();
}

La capacidad de capturar variables del entorno hace que las expresiones lambda sean extremadamente versátiles, permitiendo crear funciones que se adaptan dinámicamente al contexto donde se utilizan, lo que facilita la implementación de patrones de programación funcional en Java.

Aprende Java online

Otros ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Funciones lambda con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Arrays Y Matrices

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Excepciones

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Transformación

Programación Funcional

Reducción Y Acumulación

Programación Funcional

Mapeo

Programación Funcional

Streams Paralelos

Programación Funcional

Agrupación Y Partición

Programación Funcional

Filtrado Y Búsqueda

Programación Funcional

Api Java.nio 2

Entrada Y Salida Io

Fundamentos De Io

Entrada Y Salida Io

Leer Y Escribir Archivos

Entrada Y Salida Io

Httpclient Moderno

Entrada Y Salida Io

Clases De Nio2

Entrada Y Salida Io

Api Java.time

Api Java.time

Localtime

Api Java.time

Localdatetime

Api Java.time

Localdate

Api Java.time

Executorservice

Concurrencia

Virtual Threads (Project Loom)

Concurrencia

Future Y Completablefuture

Concurrencia

Spring Framework

Frameworks Para Java

Micronaut

Frameworks Para Java

Maven

Frameworks Para Java

Gradle

Frameworks Para Java

Lombok Para Java

Frameworks Para Java

Quarkus

Frameworks Para Java

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Introducción A Junit 5

Testing

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la sintaxis básica y variaciones de las expresiones lambda en Java.
  • Identificar los contextos de uso y el mecanismo de target typing para lambdas.
  • Entender cómo las lambdas capturan variables externas y las restricciones asociadas.
  • Diferenciar el scope de variables dentro de lambdas y el comportamiento de la palabra clave 'this'.
  • Aplicar expresiones lambda para simplificar código en colecciones, concurrencia y eventos.