Expresiones lambda en Java

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 22 min de lectura ✓ Nivel intermedio

🔷 ¿Qué son las expresiones lambda?

Una expresión lambda en Java es una función anónima —es decir, una función sin nombre— que permite implementar de forma concisa el único método abstracto de una interfaz funcional. Las lambdas representan uno de los cambios más significativos en la historia del lenguaje Java: permiten tratar el comportamiento como un dato que puede almacenarse en variables, pasarse como argumento a métodos y devolverse como resultado.

Antes de Java 8, para pasar un fragmento de comportamiento a un método (por ejemplo, un comparador a Collections.sort()), era necesario crear una clase anónima completa. Las expresiones lambda eliminan esa verbosidad y acercan Java al paradigma de la programación funcional, manteniendo plena compatibilidad con todo el código orientado a objetos existente.

La idea central es sencilla: si una interfaz tiene un único método abstracto, la lambda proporciona la implementación de ese método de forma directa, sin necesidad de la ceremonia habitual de clases e instanciación.

Antes y después de las lambdas
// ❌ Antes de Java 8: clase anónima
Comparator<String> comp1 = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

// ✅ Con lambda en Java 8+
Comparator<String> comp2 = (a, b) -> a.length() - b.length();
✅ Dato clave: Las expresiones lambda no son simples azúcar sintáctico sobre clases anónimas. Internamente, el compilador usa la instrucción invokedynamic (introducida en Java 7 para lenguajes dinámicos sobre la JVM), lo que produce código más eficiente que la generación de clases .class adicionales.

📜 Contexto histórico: Java 8 y la revolución funcional

Las expresiones lambda llegaron a Java con la versión Java SE 8, publicada el 18 de marzo de 2014. Su desarrollo se llevó a cabo dentro del Proyecto Lambda (JSR 335), liderado por Brian Goetz, arquitecto del lenguaje en Oracle. Esta versión fue posiblemente la actualización más transformadora de Java desde la introducción de los generics en Java 5 (2004).

La necesidad de lambdas en Java venía de lejos. Lenguajes como Lisp (1958) ya manejaban funciones como ciudadanos de primera clase, y lenguajes modernos de la JVM como Scala y Kotlin ofrecían esta capacidad desde sus inicios. La presión competitiva de C# (que incorporó lambdas en 2007 con LINQ) y la demanda de la comunidad fueron factores decisivos para que Oracle priorizara esta característica.

Java 8 no solo introdujo lambdas: llegó con un ecosistema completo de soporte que incluía las interfaces funcionales, la API de Streams, el paquete java.util.function y los métodos default en interfaces. Juntas, estas características transformaron la forma de escribir código Java, permitiendo un estilo más declarativo y funcional sin renunciar a la orientación a objetos.

Versión de Java Año Aportación a la programación funcional
Java 5 2004 Generics (tipado parametrizado)
Java 7 2011 invokedynamic en la JVM (base técnica para lambdas)
Java 8 2014 Lambdas, Streams, interfaces funcionales, métodos default
Java 10 2018 var para inferencia de tipos locales
Java 14-17 2020-2021 Records, sealed classes, pattern matching

⚙️ Sintaxis de las expresiones lambda

La sintaxis de una expresión lambda en Java sigue una estructura clara dividida en tres partes: la lista de parámetros, el operador flecha (->) y el cuerpo de la lambda.

Estructura general
(parámetros) -> expresión
// o bien:
(parámetros) -> { bloque de sentencias; }

▶️ Variantes de sintaxis

Java permite varias formas de escribir lambdas, desde la más explícita hasta la más concisa. El compilador infiere los tipos de los parámetros del contexto (la interfaz funcional a la que se asigna la lambda), por lo que generalmente no es necesario declararlos.

Todas las variantes sintácticas
// 1. Sin parámetros
Runnable r = () -> System.out.println("Hola");

// 2. Un parámetro (paréntesis opcionales)
Consumer<String> c = s -> System.out.println(s);
Consumer<String> c2 = (s) -> System.out.println(s);  // equivalente

// 3. Múltiples parámetros (paréntesis obligatorios)
BinaryOperator<Integer> suma = (a, b) -> a + b;

// 4. Con tipos explícitos (poco habitual)
BinaryOperator<Integer> suma2 = (Integer a, Integer b) -> a + b;

// 5. Cuerpo con múltiples líneas (llaves + return explícito)
Function<String, Integer> parsear = (texto) -> {
    texto = texto.trim();
    return Integer.parseInt(texto);
};

// 6. Lambda que no devuelve nada
Consumer<String> imprimir = msg -> {
    System.out.println("Mensaje: " + msg);
    System.out.println("Longitud: " + msg.length());
};
💡 Regla de oro: Si el cuerpo de la lambda es una sola expresión, no se necesitan llaves ni return; Java evalúa la expresión y devuelve su resultado automáticamente. Si el cuerpo tiene varias sentencias, se requieren llaves y, si la interfaz espera un valor de retorno, un return explícito.

🧩 Interfaces funcionales

Una interfaz funcional es una interfaz Java que contiene exactamente un método abstracto (conocido como SAM: Single Abstract Method). Esta restricción es lo que permite a las lambdas saber qué método están implementando. La interfaz puede tener cualquier número de métodos default o static adicionales sin perder su condición de funcional.

Java proporciona la anotación @FunctionalInterface para marcar explícitamente estas interfaces. Aunque la anotación no es obligatoria (cualquier interfaz con un solo método abstracto ya es funcional), se recomienda usarla porque el compilador generará un error si la interfaz no cumple la condición.

Definición y uso de interfaz funcional propia
@FunctionalInterface
public interface Transformador<T> {
    T transformar(T valor);

    // Métodos default permitidos: no rompen la condición funcional
    default Transformador<T> yLuego(Transformador<T> otra) {
        return valor -> otra.transformar(this.transformar(valor));
    }
}

// Uso con lambda
Transformador<String> aMayusculas = s -> s.toUpperCase();
Transformador<String> sinEspacios = s -> s.trim();

// Composición
Transformador<String> limpiar = sinEspacios.yLuego(aMayusculas);
System.out.println(limpiar.transformar("  hola mundo  "));
// Salida: HOLA MUNDO

🔹 Interfaces funcionales del JDK que ya conoces

Muchas interfaces clásicas de Java ya eran funcionales antes de Java 8, simplemente no se les llamaba así. Estas interfaces se usan habitualmente con lambdas:

Interfaz Método abstracto Uso habitual
Runnable void run() Ejecutar código en hilos
Callable<V> V call() Tareas con resultado en concurrencia
Comparator<T> int compare(T, T) Ordenar colecciones
ActionListener void actionPerformed(e) Eventos en Swing/AWT

📦 El paquete java.util.function

Java 8 introdujo el paquete java.util.function con más de 40 interfaces funcionales predefinidas. Las cuatro interfaces más importantes son los pilares sobre los que se construye gran parte de la API de Streams y la programación funcional en Java:

Interfaz Firma del método Recibe Devuelve Ejemplo de uso
Predicate<T> boolean test(T) T boolean Filtrar elementos
Function<T,R> R apply(T) T R Transformar datos
Consumer<T> void accept(T) T void Realizar acciones
Supplier<T> T get() nada T Proveer/generar valores
Las cuatro interfaces fundamentales en acción
import java.util.function.*;

public class InterfacesFuncionalesDemo {
    public static void main(String[] args) {

        // Predicate: ¿es mayor de edad?
        Predicate<Integer> esMayor = edad -> edad >= 18;
        System.out.println(esMayor.test(25));  // true
        System.out.println(esMayor.test(15));  // false

        // Function: convertir nombre a su longitud
        Function<String, Integer> longitud = String::length;
        System.out.println(longitud.apply("Lambda"));  // 6

        // Consumer: imprimir con formato
        Consumer<String> saludar = nombre ->
            System.out.println("¡Hola, " + nombre + "!");
        saludar.accept("Ana");  // ¡Hola, Ana!

        // Supplier: generar un número aleatorio
        Supplier<Double> aleatorio = Math::random;
        System.out.println(aleatorio.get());  // 0.7362... (varía)
    }
}

🔹 Variantes especializadas

El paquete incluye versiones especializadas para tipos primitivos (IntPredicate, LongFunction, DoubleConsumer...) que evitan el autoboxing y mejoran el rendimiento. También ofrece variantes binarias como BiFunction<T,U,R>, BiPredicate<T,U> y BiConsumer<T,U> para operaciones que reciben dos argumentos, y operadores como UnaryOperator<T> (extiende Function<T,T>) y BinaryOperator<T> (extiende BiFunction<T,T,T>).

✅ Buena práctica: Antes de crear tu propia interfaz funcional, revisa si ya existe una en java.util.function. En la mayoría de los casos, las interfaces estándar cubren la necesidad y hacen que tu código sea más idiomático y legible para otros desarrolladores Java.

🌊 Lambdas con colecciones y Streams

El verdadero potencial de las lambdas se manifiesta al combinarlas con la API de Streams, también introducida en Java 8. Un Stream es una secuencia de elementos sobre la que se pueden encadenar operaciones de filtrado, transformación, ordenación y reducción de forma declarativa, sin bucles explícitos.

Las lambdas son el mecanismo que alimenta cada operación del Stream: filter() recibe un Predicate, map() recibe una Function, forEach() recibe un Consumer, y así sucesivamente.

Pipeline de Streams con lambdas
import java.util.*;
import java.util.stream.*;

public class StreamsConLambdas {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList(
            "Ana", "Pedro", "María", "Juan", "Alejandra", "Luis"
        );

        // Filtrar, transformar y recolectar en una sola cadena
        List<String> resultado = nombres.stream()
            .filter(n -> n.length() > 4)           // Predicate: más de 4 letras
            .map(String::toUpperCase)                // Function: a mayúsculas
            .sorted()                                // Orden natural
            .collect(Collectors.toList());           // Recoger en lista

        System.out.println(resultado);
        // Salida: [ALEJANDRA, MARÍA, PEDRO]
    }
}

🔹 Operaciones más comunes con lambdas en Streams

Operación Tipo Lambda que recibe Propósito
filter() Intermedia Predicate<T> Conservar elementos que cumplen condición
map() Intermedia Function<T,R> Transformar cada elemento
sorted() Intermedia Comparator<T> Ordenar elementos
forEach() Terminal Consumer<T> Realizar acción sobre cada elemento
reduce() Terminal BinaryOperator<T> Combinar todos los elementos en uno
anyMatch() Terminal Predicate<T> ¿Algún elemento cumple la condición?
Ejemplo: análisis de una lista de números
List<Integer> numeros = Arrays.asList(3, 7, 12, 5, 8, 21, 14, 2);

// Suma de los pares
int sumaPares = numeros.stream()
    .filter(n -> n % 2 == 0)
    .reduce(0, Integer::sum);
System.out.println("Suma de pares: " + sumaPares);  // 36

// El mayor de los impares
Optional<Integer> maxImpar = numeros.stream()
    .filter(n -> n % 2 != 0)
    .max(Comparator.naturalOrder());
maxImpar.ifPresent(v -> System.out.println("Mayor impar: " + v));  // 21

// Convertir a strings con formato
String formateados = numeros.stream()
    .map(n -> "[" + n + "]")
    .collect(Collectors.joining(", "));
System.out.println(formateados);  // [3], [7], [12], [5], [8], [21], [14], [2]

🔗 Referencias a métodos (Method References)

Cuando una lambda simplemente llama a un método existente sin añadir lógica adicional, se puede usar una referencia a método como atajo más legible. Las referencias a métodos usan el operador :: y existen cuatro tipos:

Tipo Sintaxis Lambda equivalente Ejemplo
Método estático Clase::metodo x -> Clase.metodo(x) Integer::parseInt
Método de instancia (objeto concreto) obj::metodo x -> obj.metodo(x) System.out::println
Método de instancia (tipo arbitrario) Clase::metodo x -> x.metodo() String::toUpperCase
Constructor Clase::new x -> new Clase(x) ArrayList::new
Comparación lambda vs method reference
List<String> palabras = Arrays.asList("java", "lambda", "stream");

// Lambda explícita
palabras.forEach(p -> System.out.println(p));

// Method reference equivalente (más idiomático)
palabras.forEach(System.out::println);

// Ordenar por longitud — lambda
palabras.sort((a, b) -> Integer.compare(a.length(), b.length()));

// Ordenar por longitud — method reference con Comparator
palabras.sort(Comparator.comparingInt(String::length));

// Convertir strings a enteros — lambda vs method reference
Function<String, Integer> parser1 = s -> Integer.parseInt(s);
Function<String, Integer> parser2 = Integer::parseInt;  // más limpio
💡 Consejo: Usa method references cuando la lambda simplemente delega en un método existente. Si necesitas añadir lógica (como concatenar cadenas o hacer cálculos), mantén la lambda.

🔒 Closures y captura de variables

Las lambdas en Java pueden acceder a variables del ámbito que las rodea, comportándose como closures. Sin embargo, Java impone una restricción importante: solo pueden capturar variables locales que sean efectivamente finales (effectively final), es decir, variables cuyo valor no cambia después de la inicialización.

Captura de variables: lo permitido y lo prohibido
public class ClosureDemo {
    private int atributo = 10;  // Los atributos de instancia SÍ pueden cambiar

    public void ejemplo() {
        int multiplicador = 3;  // Effectively final: nunca se reasigna

        // ✅ Correcto: captura variable effectively final
        Function<Integer, Integer> triplicar = n -> n * multiplicador;
        System.out.println(triplicar.apply(5));  // 15

        // ✅ Correcto: captura atributo de instancia (acceso vía this)
        Consumer<Integer> sumar = n -> this.atributo += n;
        sumar.accept(5);
        System.out.println(this.atributo);  // 15

        // ❌ ERROR DE COMPILACIÓN:
        // int contador = 0;
        // Runnable inc = () -> contador++;  // No compila: contador no es effectively final
    }
}

🔹 ¿Por qué esta restricción?

Las variables locales viven en la pila (stack) del hilo que ejecuta el método. Cuando el método termina, la variable desaparece. Pero la lambda puede sobrevivir al método (por ejemplo, si se almacena en una colección o se pasa a otro hilo). Java resuelve esto copiando el valor de la variable capturada en la lambda. Si la variable pudiera cambiar después de la copia, habría inconsistencia entre el valor en la pila y el valor en la lambda, lo que conduciría a bugs sutiles y difíciles de detectar.

⚠️ Truco habitual (pero cuidado): Algunos desarrolladores usan arrays de un elemento (int[] contador = {0}) o AtomicInteger para sortear la restricción. Esto funciona porque la referencia al array es effectively final (el array no cambia), aunque su contenido sí. Es una práctica válida en contextos simples, pero puede generar problemas en concurrencia si la lambda se ejecuta en otro hilo.

🛒 Ejemplo integrador: sistema de filtrado de productos

Este ejemplo muestra cómo las lambdas, las interfaces funcionales y los Streams trabajan juntos en un caso realista: un sistema de comercio electrónico que necesita filtrar, transformar y presentar productos según criterios dinámicos.

Sistema completo de filtrado de productos
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

// Modelo de datos
class Producto {
    private String nombre;
    private String categoria;
    private double precio;
    private int stock;

    public Producto(String nombre, String categoria, double precio, int stock) {
        this.nombre = nombre;
        this.categoria = categoria;
        this.precio = precio;
        this.stock = stock;
    }

    // Getters
    public String getNombre()    { return nombre; }
    public String getCategoria() { return categoria; }
    public double getPrecio()    { return precio; }
    public int getStock()        { return stock; }

    @Override
    public String toString() {
        return String.format("%s (%.2f€, stock: %d)", nombre, precio, stock);
    }
}

// Motor de búsqueda con lambdas
class BuscadorProductos {

    // Método genérico que acepta cualquier combinación de filtros
    public static List<Producto> buscar(
            List<Producto> catalogo,
            Predicate<Producto> filtro,
            Comparator<Producto> orden,
            int limite) {

        return catalogo.stream()
            .filter(filtro)
            .sorted(orden)
            .limit(limite)
            .collect(Collectors.toList());
    }

    // Fábrica de predicados reutilizables
    public static Predicate<Producto> precioEntre(double min, double max) {
        return p -> p.getPrecio() >= min && p.getPrecio() <= max;
    }

    public static Predicate<Producto> categoriaEs(String cat) {
        return p -> p.getCategoria().equalsIgnoreCase(cat);
    }

    public static Predicate<Producto> conStock() {
        return p -> p.getStock() > 0;
    }
}

// Programa principal
public class TiendaLambda {
    public static void main(String[] args) {
        List<Producto> catalogo = Arrays.asList(
            new Producto("Teclado mecánico",  "Periféricos",  89.99, 15),
            new Producto("Monitor 27\"",      "Monitores",   349.00,  3),
            new Producto("Ratón inalámbrico", "Periféricos",  45.50, 42),
            new Producto("SSD 1TB",           "Almacenamiento", 79.99, 0),
            new Producto("Webcam HD",         "Periféricos",  62.00, 28),
            new Producto("RAM 16GB DDR5",     "Componentes", 125.00, 11),
            new Producto("Monitor 24\"",      "Monitores",   199.00,  7),
            new Producto("Hub USB-C",         "Periféricos",  34.99, 55)
        );

        // Buscar periféricos con stock, entre 30€ y 70€, ordenados por precio
        Predicate<Producto> filtro = BuscadorProductos.categoriaEs("Periféricos")
            .and(BuscadorProductos.conStock())
            .and(BuscadorProductos.precioEntre(30, 70));

        List<Producto> resultados = BuscadorProductos.buscar(
            catalogo,
            filtro,
            Comparator.comparingDouble(Producto::getPrecio),
            10
        );

        System.out.println("=== Periféricos (30-70€, con stock) ===");
        resultados.forEach(System.out::println);

        // Calcular el valor total del stock
        double valorTotal = catalogo.stream()
            .filter(BuscadorProductos.conStock())
            .mapToDouble(p -> p.getPrecio() * p.getStock())
            .sum();
        System.out.printf("\nValor total en stock: %.2f€%n", valorTotal);

        // Resumen por categoría
        Map<String, Long> porCategoria = catalogo.stream()
            .collect(Collectors.groupingBy(
                Producto::getCategoria,
                Collectors.counting()
            ));
        System.out.println("\nProductos por categoría: " + porCategoria);
    }
}

/* Salida esperada:
=== Periféricos (30-70€, con stock) ===
Hub USB-C (34,99€, stock: 55)
Ratón inalámbrico (45,50€, stock: 42)
Webcam HD (62,00€, stock: 28)

Valor total en stock: 14.384,27€

Productos por categoría: {Monitores=2, Almacenamiento=1, Periféricos=4, Componentes=1}
*/

⚠️ Errores frecuentes con lambdas

Error Código incorrecto Solución
Modificar variable local capturada int c = 0; list.forEach(x -> c++); Usar AtomicInteger o reduce()
Olvidar return en bloque x -> { x * 2; } x -> { return x * 2; } o quitar llaves: x -> x * 2
Lambda en interfaz no funcional Asignar lambda a interfaz con 2+ métodos abstractos Verificar que la interfaz tiene exactamente 1 método abstracto
Excepciones checked en lambdas list.forEach(f -> new FileReader(f)); Envolver en try-catch dentro de la lambda o usar wrapper
Confundir tipos en method reference String::compareTo como Comparator<String> Verificar la firma: compareTo es (String) -> int, no (String, String) -> int
Manejo de excepciones checked en lambdas
// ❌ No compila: FileReader lanza IOException (checked)
// list.forEach(f -> new FileReader(f));

// ✅ Solución 1: try-catch dentro de la lambda
list.forEach(f -> {
    try {
        new FileReader(f);
    } catch (IOException e) {
        throw new RuntimeException("Error al abrir: " + f, e);
    }
});

// ✅ Solución 2: método wrapper reutilizable
@FunctionalInterface
interface CheckedFunction<T, R> {
    R apply(T t) throws Exception;
}

static <T, R> Function<T, R> unchecked(CheckedFunction<T, R> fn) {
    return t -> {
        try {
            return fn.apply(t);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    };
}

// Uso limpio
list.stream()
    .map(unchecked(f -> new FileReader(f)))
    .forEach(System.out::println);

📝 Ejercicios prácticos

Ejercicio 1 — Filtrado con Predicate (Principiante)

Dada una lista de enteros, usa una lambda con filter() para obtener solo los números mayores que 10 y divisibles entre 3. Imprime el resultado.

Ver solución
Solución Ejercicio 1
import java.util.*;
import java.util.stream.*;

public class Ejercicio1 {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(5, 12, 7, 15, 3, 21, 9, 30, 18, 4);

        List<Integer> resultado = nums.stream()
            .filter(n -> n > 10 && n % 3 == 0)
            .collect(Collectors.toList());

        System.out.println(resultado);  // [12, 15, 21, 30, 18]
    }
}

Ejercicio 2 — Transformación con Function (Intermedio)

Crea un método aplicarTransformaciones que reciba una lista de strings y una lista de Function<String, String>, y aplique todas las transformaciones en secuencia a cada string. Prueba con: recortar espacios, convertir a mayúsculas, y añadir un prefijo «[PROCESADO] ».

Ver solución
Solución Ejercicio 2
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Ejercicio2 {

    public static List<String> aplicarTransformaciones(
            List<String> datos,
            List<Function<String, String>> transformaciones) {

        // Componer todas las funciones en una sola
        Function<String, String> pipeline = transformaciones.stream()
            .reduce(Function.identity(), Function::andThen);

        return datos.stream()
            .map(pipeline)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<String> datos = Arrays.asList("  hola  ", " mundo ", "  java  ");

        List<Function<String, String>> ops = Arrays.asList(
            String::trim,
            String::toUpperCase,
            s -> "[PROCESADO] " + s
        );

        List<String> resultado = aplicarTransformaciones(datos, ops);
        resultado.forEach(System.out::println);
        // [PROCESADO] HOLA
        // [PROCESADO] MUNDO
        // [PROCESADO] JAVA
    }
}

Ejercicio 3 — Sistema de validación con Predicate (Avanzado)

Diseña un sistema de validación para objetos Usuario (nombre, email, edad) usando composición de Predicate. Cada regla es un predicado independiente. El método validar debe recibir un usuario y una lista de predicados, devolviendo una lista de mensajes de error (strings). Si todos los predicados se cumplen, la lista queda vacía.

Ver solución
Solución Ejercicio 3
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

class Usuario {
    String nombre;
    String email;
    int edad;

    Usuario(String nombre, String email, int edad) {
        this.nombre = nombre;
        this.email = email;
        this.edad = edad;
    }
}

// Par de (predicado, mensaje de error si falla)
class Regla<T> {
    Predicate<T> condicion;
    String mensajeError;

    Regla(Predicate<T> condicion, String mensajeError) {
        this.condicion = condicion;
        this.mensajeError = mensajeError;
    }
}

class Validador {
    public static <T> List<String> validar(T objeto, List<Regla<T>> reglas) {
        return reglas.stream()
            .filter(r -> !r.condicion.test(objeto))  // reglas que NO se cumplen
            .map(r -> r.mensajeError)
            .collect(Collectors.toList());
    }
}

public class Ejercicio3 {
    public static void main(String[] args) {
        List<Regla<Usuario>> reglas = Arrays.asList(
            new Regla<>(u -> u.nombre != null && !u.nombre.isBlank(),
                        "El nombre no puede estar vacío"),
            new Regla<>(u -> u.email != null && u.email.contains("@"),
                        "El email debe contener @"),
            new Regla<>(u -> u.edad >= 18,
                        "Debe ser mayor de edad"),
            new Regla<>(u -> u.edad <= 120,
                        "La edad no es válida")
        );

        Usuario u1 = new Usuario("Ana", "ana@mail.com", 25);
        Usuario u2 = new Usuario("", "sin-arroba", 15);

        System.out.println("Usuario 1: " + Validador.validar(u1, reglas));
        // []

        System.out.println("Usuario 2: " + Validador.validar(u2, reglas));
        // [El nombre no puede estar vacío, El email debe contener @, Debe ser mayor de edad]
    }
}

❓ Preguntas frecuentes sobre Expresiones lambda en Java

Las dudas más comunes respondidas de forma clara y directa.

Una expresión lambda es una función anónima que permite implementar de forma concisa el único método abstracto de una interfaz funcional. Se introdujeron en Java 8 (2014) y su sintaxis básica es: (parámetros) -> cuerpo. Permiten tratar el comportamiento como datos, pasando funciones como argumentos a otros métodos.
Ambas permiten implementar interfaces sin crear una clase con nombre, pero las lambdas son más concisas, no crean un nuevo ámbito para this (this se refiere a la clase contenedora), y el compilador las optimiza mejor internamente usando invokedynamic en vez de generar una clase .class adicional. Las clases anónimas, en cambio, pueden implementar interfaces con múltiples métodos abstractos.
Es una interfaz que tiene exactamente un método abstracto (SAM: Single Abstract Method). Puede tener varios métodos default o static adicionales. Se recomienda anotarla con @FunctionalInterface para que el compilador verifique esta restricción. Ejemplos del JDK: Predicate, Function, Consumer y Supplier.
Sí, pero solo si la variable es efectivamente final (effectively final), es decir, no se modifica tras su inicialización. Esta restricción existe porque la lambda puede ejecutarse en un hilo diferente o después de que el método contenedor haya terminado, y Java necesita garantizar la seguridad de los datos capturados.
Predicate recibe un argumento y devuelve boolean (para filtrar). Function recibe un argumento de tipo T y devuelve un resultado de tipo R (para transformar). Consumer recibe un argumento y no devuelve nada, void (para efectos secundarios como imprimir). Supplier no recibe argumentos y devuelve un valor de tipo T (para generar o proveer valores).
Es un atajo sintáctico para lambdas que simplemente invocan un método existente. Usa el operador :: y tiene cuatro formas: referencia a método estático (Clase::metodoEstatico), referencia a método de instancia de un objeto particular (objeto::metodo), referencia a método de instancia de un tipo arbitrario (Clase::metodo) y referencia a constructor (Clase::new).
No en sentido estricto. Java sigue siendo un lenguaje orientado a objetos con soporte para programación funcional. Las lambdas, junto con la API de Streams y las interfaces funcionales, permiten escribir código en estilo funcional (funciones como valores, composición, inmutabilidad), pero Java no tiene todas las características de un lenguaje funcional puro como Haskell, por ejemplo currying nativo o pattern matching completo.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Expresiones lambda en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!