Saltar a contenido

⚡ Expresiones lambda en Java

Las expresiones lambda permiten escribir código más corto, más limpio y más flexible cuando queremos pasar una acción como parámetro.

Dicho de forma sencilla: una lambda es una forma rápida de escribir el comportamiento de un método sin crear una clase completa.


🧠 Idea principal: ¿qué problema resuelven las lambdas?

Antes de Java 8, cuando queríamos pasar un comportamiento como parámetro, muchas veces teníamos que crear una clase anónima.

Por ejemplo, imagina que queremos ordenar una lista de nombres por longitud.

List<String> nombres = new ArrayList<>();
nombres.add("Ana");
nombres.add("Fernando");
nombres.add("Luis");

Sin lambda, podríamos hacer esto:

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

Funciona, pero es muy largo para algo tan sencillo.

Con lambda:

nombres.sort((a, b) -> Integer.compare(a.length(), b.length()));

Mucho más directo.

📌 La lambda permite centrarnos en lo importante: qué queremos hacer, no en toda la estructura repetitiva que exige Java.


🧩 ¿Qué es exactamente una lambda?

Una expresión lambda es una forma compacta de implementar un método abstracto de una interfaz funcional.

La idea básica es esta:

(parametros) -> cuerpo

Ejemplo:

(a, b) -> a + b

Esta lambda recibe dos valores y devuelve su suma.

Pero cuidado: una lambda no vive sola. Java necesita saber a qué tipo pertenece.

Por eso normalmente se guarda en una variable cuyo tipo es una interfaz funcional:

Operacion suma = (a, b) -> a + b;

🧱 Antes de seguir: interfaz funcional

Una interfaz funcional es una interfaz que tiene un único método abstracto.

Ejemplo:

@FunctionalInterface
interface Operacion {
    int calcular(int a, int b);
}

Esta interfaz tiene un solo método abstracto:

int calcular(int a, int b);

Por tanto, se puede implementar con una lambda:

Operacion suma = (a, b) -> a + b;
System.out.println(suma.calcular(4, 3)); // 7

📌 La lambda está dando cuerpo al método calcular.

Es como si Java entendiera esto:

calcular(int a, int b) {
    return a + b;
}

Pero escrito de forma corta.


🏷️ La anotación @FunctionalInterface

La anotación @FunctionalInterface NO es obligatoria, pero es muy recomendable.

Sirve para que el compilador compruebe que la interfaz realmente tiene un único método abstracto.

@FunctionalInterface
interface Saludador {
    void saludar(String nombre);
}

Si intentamos añadir otro método abstracto:

@FunctionalInterface
interface Saludador {
    void saludar(String nombre);
    void despedir(String nombre); // Error
}

Java dará error porque ya no sería una interfaz funcional, ya que tiene más de UN método abstracto.

✅ Buena práctica: si una interfaz está pensada para usarse con lambdas, pon @FunctionalInterface.


🔎 Métodos abstractos, default y static

Una interfaz funcional puede tener:

  • Un único método abstracto.
  • Métodos default.
  • Métodos static.

Ejemplo válido:

@FunctionalInterface
interface Validador {
    boolean validar(String texto);

    default void mostrarInfo() {
        System.out.println("Validador de textos");
    }

    static void ayuda() {
        System.out.println("Devuelve true o false según el texto sea válido");
    }
}

Sigue siendo funcional porque solo tiene un método abstracto:

boolean validar(String texto);

✍️ Sintaxis básica de una lambda

La estructura general de una expresión lambda consta de tres componentes:

  1. Parámetros
  2. Token de flecha
  3. Cuerpo
(parametros) -> expresion

O también:

(parametros) -> {
    instrucciones;
}

La flecha -> separa:

  • A la izquierda: los parámetros.
  • A la derecha: lo que hace la lambda.

Ejemplo:

x -> x * 2

Significa:

Recibe x y devuelve x * 2.


0️⃣ Lambda sin parámetros

Si una lambda no recibe parámetros, se escribe con paréntesis vacíos obligatorios.

@FunctionalInterface
interface Mensaje {
    void mostrar();
}
Mensaje m = () -> System.out.println("Hola desde una lambda");
m.mostrar();

Salida:

Hola desde una lambda

📌 Los paréntesis son obligatorios cuando no hay parámetros.

() -> System.out.println("Hola")

1️⃣ Lambda con un parámetro

Si hay un único parámetro, se pueden omitir los paréntesis.

@FunctionalInterface
interface Saludador {
    void saludar(String nombre);
}

Con paréntesis:

Saludador s1 = (nombre) -> System.out.println("Hola, " + nombre);

Sin paréntesis:

Saludador s2 = nombre -> System.out.println("Hola, " + nombre);

Ambas formas son correctas.

s1.saludar("Ana");
s2.saludar("Luis");

Salida:

Hola, Ana
Hola, Luis

📌 Si solo hay un parámetro y no se indica el tipo, los paréntesis son opcionales.


2️⃣ Lambda con varios parámetros

Si hay varios parámetros, los paréntesis son obligatorios.

@FunctionalInterface
interface Operacion {
    int calcular(int a, int b);
}
Operacion suma = (a, b) -> a + b;
Operacion resta = (a, b) -> a - b;
Operacion multiplicacion = (a, b) -> a * b;

System.out.println(suma.calcular(5, 3));           // 8
System.out.println(resta.calcular(5, 3));          // 2
System.out.println(multiplicacion.calcular(5, 3)); // 15

🧠 ¿Tengo que poner los tipos de los parámetros?

Normalmente no.

Java suele inferirlos a partir de la interfaz funcional.

Operacion suma = (a, b) -> a + b;

Java sabe que a y b son int porque el método de la interfaz es:

int calcular(int a, int b);

También podríamos escribirlo así:

Operacion suma = (int a, int b) -> a + b;

Pero suele ser más limpio no poner los tipos.

⚠️ Lo que no se puede hacer es mezclar parámetros con tipo y sin tipo:

Operacion suma = (int a, b) -> a + b; // Error

O los pones todos:

Operacion suma = (int a, int b) -> a + b;

O no pones ninguno:

Operacion suma = (a, b) -> a + b;

↩️ Lambdas que devuelven un valor

Si el cuerpo tiene una sola expresión, no hace falta escribir return.

Operacion suma = (a, b) -> a + b;

Esto devuelve automáticamente a + b.

También se puede escribir con llaves. Si se ponen las llaves sí es necesario escribir return:

Operacion suma = (a, b) -> {
    return a + b;
};

📌 Regla importante:

  • Sin llaves {} → no se escribe return.
  • Con llaves {} → si hay que devolver algo, se escribe return.

Correcto:

(a, b) -> a + b

Correcto:

(a, b) -> {
    return a + b;
}

Incorrecto:

(a, b) -> return a + b; // Error

Incorrecto:

(a, b) -> {
    a + b; // Error, falta return
}

🧾 Lambdas con varias instrucciones

Cuando una lambda tiene varias instrucciones, hay que usar llaves.

Operacion sumaMostrando = (a, b) -> {
    int resultado = a + b;
    System.out.println("Calculando suma...");
    return resultado;
};

System.out.println(sumaMostrando.calcular(4, 6));

Salida:

Calculando suma...
10

🧪 Ejemplo completo desde cero

@FunctionalInterface
interface Transformador {
    String transformar(String texto);
}

public class Main {
    public static void main(String[] args) {
        Transformador mayusculas = texto -> texto.toUpperCase();
        Transformador admiracion = texto -> "¡" + texto + "!";
        Transformador resumen = texto -> texto.substring(0, 3);

        System.out.println(mayusculas.transformar("java"));
        System.out.println(admiracion.transformar("hola"));
        System.out.println(resumen.transformar("programacion"));
    }
}

Salida:

JAVA
¡hola!
pro

📌 La misma interfaz permite guardar comportamientos distintos.


🆚 Antes y después: clase anónima frente a lambda

😵 Sin lambda

@FunctionalInterface
interface Filtro {
    boolean aceptar(int numero);
}

public class Main {
    public static void main(String[] args) {
        Filtro pares = new Filtro() {
            @Override
            public boolean aceptar(int numero) {
                return numero % 2 == 0;
            }
        };

        System.out.println(pares.aceptar(8));
    }
}

😎 Con lambda

@FunctionalInterface
interface Filtro {
    boolean aceptar(int numero);
}

public class Main {
    public static void main(String[] args) {
        Filtro pares = numero -> numero % 2 == 0;

        System.out.println(pares.aceptar(8));
    }
}

Salida:

true

📌 Las dos versiones hacen lo mismo, pero la lambda elimina mucho código repetitivo.


🧺 Lambdas con colecciones

Las lambdas se usan muchísimo con colecciones.

Vamos a partir de esta lista:

List<String> nombres = new ArrayList<>();
nombres.add("Ana");
nombres.add("Fernando");
nombres.add("Luis");
nombres.add("Beatriz");

🔁 forEach

El método forEach permite recorrer una colección aplicando una acción a cada elemento.

nombres.forEach(nombre -> System.out.println(nombre));

Salida:

Ana
Fernando
Luis
Beatriz

La lambda:

nombre -> System.out.println(nombre)

significa:

Para cada nombre, imprímelo por pantalla.


🧹 removeIf

El método removeIf elimina los elementos que cumplen una condición.

nombres.removeIf(nombre -> nombre.length() <= 3);
System.out.println(nombres);

Salida:

[Fernando, Beatriz]

La lambda:

nombre -> nombre.length() <= 3

significa:

Elimina los nombres cuya longitud sea menor o igual que 3.


📊 sort con lambda

Podemos ordenar una lista usando una lambda.

nombres.sort((a, b) -> a.compareTo(b));
System.out.println(nombres);

Ordena alfabéticamente.

También podemos ordenar por longitud:

nombres.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(nombres);

📌 Aquí la lambda representa el criterio de comparación.


🏗️ Lambdas con objetos

Supongamos esta clase:

class Producto {
    private String nombre;
    private double precio;

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

    public String getNombre() {
        return nombre;
    }

    public double getPrecio() {
        return precio;
    }

    @Override
    public String toString() {
        return nombre + " - " + precio + "€";
    }
}

Creamos una lista:

List<Producto> productos = new ArrayList<>();
productos.add(new Producto("Teclado", 30));
productos.add(new Producto("Ratón", 15));
productos.add(new Producto("Monitor", 120));
productos.add(new Producto("USB", 8));

🧹 Filtrar objetos con removeIf

Eliminar productos baratos:

productos.removeIf(p -> p.getPrecio() < 20);
System.out.println(productos);

Salida:

[Teclado - 30.0€, Monitor - 120.0€]

📊 Ordenar objetos con sort

Ordenar por precio ascendente:

productos.sort((p1, p2) -> Double.compare(p1.getPrecio(), p2.getPrecio()));

Ordenar por nombre:

productos.sort((p1, p2) -> p1.getNombre().compareTo(p2.getNombre()));

🧰 Interfaces funcionales ya preparadas por Java

Java ya trae muchas interfaces funcionales en el paquete:

java.util.function

Las más importantes para empezar son:

Interfaz Recibe Devuelve Idea
Predicate<T> T boolean Comprobar una condición
Consumer<T> T nada (void) Consumir o usar un dato
Supplier<T> nada T Producir un dato
Function<T, R> T R Transformar un dato en otro
BiFunction<T, U, R> T, U R Transformar dos datos en otro
UnaryOperator<T> T T Transformar un dato en otro del mismo tipo
BinaryOperator<T> T, T T Combinar dos datos del mismo tipo

No hace falta crear una interfaz propia para todo. Muchas veces podemos usar estas.


Predicate<T>: comprobar condiciones

Un Predicate<T> recibe un dato y devuelve true o false.

import java.util.function.Predicate;

Predicate<String> esLargo = texto -> texto.length() > 5;

System.out.println(esLargo.test("Java"));        // false
System.out.println(esLargo.test("Programar"));   // true

El método de Predicate se llama:

test()

📌 Se usa mucho para filtros y validaciones.

Ejemplo:

Predicate<Integer> esPar = n -> n % 2 == 0;
Predicate<Integer> esPositivo = n -> n > 0;

System.out.println(esPar.test(8));       // true
System.out.println(esPositivo.test(-3)); // false

🧩 Combinar Predicate

Los predicados se pueden combinar con:

  • and()
  • or()
  • negate()
Predicate<Integer> esPar = n -> n % 2 == 0;
Predicate<Integer> esPositivo = n -> n > 0;

Predicate<Integer> parYPositivo = esPar.and(esPositivo);

System.out.println(parYPositivo.test(10)); // true
System.out.println(parYPositivo.test(-4)); // false

Con or():

Predicate<Integer> parONegativo = esPar.or(n -> n < 0);

System.out.println(parONegativo.test(7));  // false
System.out.println(parONegativo.test(-3)); // true

Con negate():

Predicate<Integer> impar = esPar.negate();

System.out.println(impar.test(5)); // true

//se pueden combinar varios
Predicate<Integer> greaterThan10 = i -> i > 10;
Predicate<Integer> lessThan20 = i -> i < 20;
System.out.println(greaterThan10.and(lessThan20).test(15));

Predicate<Integer> greaterThanTen = (i) -> i > 10;
Predicate<Integer> lowerThanTwenty = (i) -> i < 20;
boolean resul = greaterThanTen.and(lowerThanTwenty).test(15);//true
boolean resul2 = greaterThanTen.and(lowerThanTwenty).negate().test(15);//false

Predicate<String> i  = Predicate.isEqual("asdf");
System.out.println(i.test("java"));//false

📦 Consumer<T>: hacer algo con un dato

Un Consumer<T> recibe un dato y no devuelve nada.

import java.util.function.Consumer;

Consumer<String> imprimir = texto -> System.out.println(texto);

imprimir.accept("Hola");

El método de Consumer se llama:

accept()

Ejemplo con lista:

List<String> nombres = List.of("Ana", "Luis", "Marta");

Consumer<String> mostrar = nombre -> System.out.println("Nombre: " + nombre);

nombres.forEach(mostrar);

Salida:

Nombre: Ana
Nombre: Luis
Nombre: Marta

📌 Consumer sirve para acciones: imprimir, guardar, modificar, registrar, etc.


🎁 Supplier<T>: producir un dato

Un Supplier<T> no recibe nada y devuelve un dato.

import java.util.function.Supplier;

Supplier<Double> aleatorio = () -> Math.random();

System.out.println(aleatorio.get());

El método de Supplier se llama:

get()

Ejemplo:

Supplier<String> saludo = () -> "Hola, mundo";

System.out.println(saludo.get());

📌 Supplier sirve para generar o proporcionar valores.


🔄 Function<T, R>: transformar un dato

Una Function<T, R> recibe un dato de tipo T y devuelve un dato de tipo R.

import java.util.function.Function;

Function<String, Integer> longitud = texto -> texto.length();

System.out.println(longitud.apply("Java")); // 4

El método de Function se llama:

apply()

La parte <String, Integer> significa:

Function<tipoEntrada, tipoSalida>

En este caso:

Function<String, Integer>

significa:

Recibe un String y devuelve un Integer.


🔗 Combinar Function

Las funciones se pueden encadenar con:

  • andThen()
  • compose()

Ejemplo con andThen():

Function<String, String> limpiar = texto -> texto.trim();
Function<String, String> mayusculas = texto -> texto.toUpperCase();

Function<String, String> limpiarYMayusculas = limpiar.andThen(mayusculas);

System.out.println(limpiarYMayusculas.apply("  java  ")); // JAVA

Primero se ejecuta limpiar, después mayusculas.

Ejemplo con compose():

Function<String, String> resultado = mayusculas.compose(limpiar);

System.out.println(resultado.apply("  java  ")); // JAVA

Con compose, primero se ejecuta lo que va dentro del paréntesis.


🧮 BiFunction<T, U, R>: dos entradas, una salida

BiFunction recibe dos datos y devuelve uno.

import java.util.function.BiFunction;

BiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;

System.out.println(suma.apply(4, 5)); // 9

Significa:

BiFunction<tipoEntrada1, tipoEntrada2, tipoSalida>

Ejemplo con tipos distintos:

BiFunction<String, Integer, String> repetir = (texto, veces) -> texto.repeat(veces);

System.out.println(repetir.apply("ja", 3)); // jajaja

🔁 UnaryOperator<T> y BinaryOperator<T>

Son casos especiales de Function y BiFunction.

UnaryOperator<T>

Recibe un dato y devuelve otro del mismo tipo.

import java.util.function.UnaryOperator;

UnaryOperator<String> duplicar = texto -> texto + texto;

System.out.println(duplicar.apply("Java")); // JavaJava

BinaryOperator<T>

Recibe dos datos del mismo tipo y devuelve uno del mismo tipo.

import java.util.function.BinaryOperator;

BinaryOperator<Integer> mayor = (a, b) -> a > b ? a : b;

System.out.println(mayor.apply(8, 3)); // 8

🏃 Interfaces funcionales para tipos primitivos

Las interfaces anteriores usan objetos: Integer, Double, Boolean, etc.

A veces Java ofrece versiones especiales para evitar conversiones entre primitivos y wrappers.

Ejemplos:

Interfaz Uso
IntPredicate Condición sobre un int
IntConsumer Acción sobre un int
IntSupplier Produce un int
IntFunction<R> Recibe int y devuelve R
IntUnaryOperator Recibe int y devuelve int
IntBinaryOperator Recibe dos int y devuelve int
DoublePredicate Condición sobre un double
LongSupplier Produce un long

Ejemplo:

import java.util.function.IntPredicate;

IntPredicate esPar = n -> n % 2 == 0;

System.out.println(esPar.test(6)); // true

📌 Para empezar, no es obligatorio usarlas, pero conviene saber que existen.


🧭 Inferencia de tipo y tipo objetivo

Una lambda necesita un tipo objetivo.

Esto significa que Java debe saber qué interfaz funcional está implementando la lambda.

Correcto:

Predicate<String> empiezaPorA = texto -> texto.startsWith("A");

Java sabe que la lambda pertenece a Predicate<String>.

Incorrecto:

var filtro = texto -> texto.startsWith("A"); // Error

¿Por qué?

Porque con var Java no sabe qué interfaz funcional quieres usar.

Sí se puede hacer esto:

Predicate<String> filtro = texto -> texto.startsWith("A");
var otroFiltro = filtro;

📌 La lambda necesita primero un tipo claro.


🔒 Variables externas en lambdas

Una lambda puede usar variables locales externas, pero esas variables deben ser finales o efectivamente finales.

Ejemplo correcto:

public void ejemplo() {
    String prefijo = "Hola";

    Consumer<String> saludar = nombre -> System.out.println(prefijo + ", " + nombre);

    saludar.accept("Ana");
}

Aunque prefijo no tenga la palabra final, no se modifica después. Por eso es efectivamente final.

Ejemplo incorrecto:

public void ejemplo() {
    String prefijo = "Hola";

    Consumer<String> saludar = nombre -> System.out.println(prefijo + ", " + nombre);

    prefijo = "Buenas"; // Error si la lambda usa prefijo
}

📌 Si una variable local se usa dentro de una lambda, no puedes cambiarla después. Porque una lambda puede ejecutarse más tarde, cuando el método donde se creó ya ha terminado.


🧨 Error típico: intentar modificar una variable local

Esto no compila:

public void ejemplo() {
    List<String> nombres = List.of("Ana", "Luis", "Marta");
    int contador = 0;

    nombres.forEach(nombre -> {
        contador++; // Error
    });
}

¿Por qué?

Porque contador es una variable local externa y la lambda intenta modificarla.

Soluciones posibles:

✅ Usar un bucle normal

int contador = 0;

for (String nombre : nombres) {
    contador++;
}

System.out.println(contador);

✅ Usar una estructura mutable

int[] contador = {0};

nombres.forEach(nombre -> contador[0]++);

System.out.println(contador[0]);

Aunque funciona, no siempre es la solución más elegante.

✅ Usar Streams más adelante

long contador = nombres.stream().count();

📌 Lambdas y this

En una clase anónima, this se refiere al objeto de la clase anónima.

En una lambda, this se refiere al objeto externo donde está escrita la lambda.

Ejemplo:

class Ejemplo {
    private String nombre = "Objeto externo";

    public void probar() {
        Runnable r = () -> System.out.println(this.nombre);
        r.run();
    }
}

Salida:

Objeto externo

📌 En lambdas, this no crea un nuevo contexto como una clase anónima.


📊 Lambdas modernas con Comparator

Una de las aplicaciones más útiles de las lambdas es ordenar objetos.

Supongamos esta clase:

class Persona {
    private String nombre;
    private int edad;

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    public String getNombre() {
        return nombre;
    }

    public int getEdad() {
        return edad;
    }

    @Override
    public String toString() {
        return nombre + " (" + edad + ")";
    }
}

Lista:

List<Persona> personas = new ArrayList<>();
personas.add(new Persona("Ana", 25));
personas.add(new Persona("Luis", 19));
personas.add(new Persona("Marta", 30));

📈 Ordenar con lambda clásica

personas.sort((p1, p2) -> Integer.compare(p1.getEdad(), p2.getEdad()));

Ordena por edad ascendente.


✨ Ordenar con Comparator.comparing

Forma más moderna y legible:

personas.sort(Comparator.comparing(Persona::getEdad));

Ordenar por nombre:

personas.sort(Comparator.comparing(Persona::getNombre));

Ordenar por edad descendente:

personas.sort(Comparator.comparing(Persona::getEdad).reversed());

Ordenar por edad y, si empatan, por nombre:

personas.sort(
    Comparator.comparing(Persona::getEdad)
              .thenComparing(Persona::getNombre)
);

📌 Esta forma se usa muchísimo en Java moderno.


🧹 Lambdas y Streams: pequeña introducción

Los Streams se verán con más detalle después, pero las lambdas son la base para usarlos.

Ejemplo:

List<String> nombres = List.of("Ana", "Fernando", "Luis", "Beatriz");

nombres.stream()
       .filter(nombre -> nombre.length() > 3)
       .map(nombre -> nombre.toUpperCase())
       .forEach(nombre -> System.out.println(nombre));

Salida:

FERNANDO
LUIS
BEATRIZ

¿Qué hace cada lambda?

nombre -> nombre.length() > 3

Filtra los nombres de más de 3 letras.

nombre -> nombre.toUpperCase()

Transforma cada nombre a mayúsculas.

nombre -> System.out.println(nombre)

Imprime cada nombre.

Con referencias a métodos:

nombres.stream()
       .filter(nombre -> nombre.length() > 3)
       .map(String::toUpperCase)
       .forEach(System.out::println);

🧪 Ejemplo práctico: sistema de descuentos

Queremos aplicar distintos descuentos a un precio.

@FunctionalInterface
interface Descuento {
    double aplicar(double precio);
}
public class Main {
    public static void main(String[] args) {
        Descuento sinDescuento = precio -> precio;
        Descuento diezPorCiento = precio -> precio * 0.90;
        Descuento mitad = precio -> precio * 0.50;

        double precio = 100;

        System.out.println(sinDescuento.aplicar(precio)); // 100.0
        System.out.println(diezPorCiento.aplicar(precio)); // 90.0
        System.out.println(mitad.aplicar(precio));         // 50.0
    }
}

📌 Tenemos tres comportamientos distintos guardados en variables.


🧪 Ejemplo práctico: validaciones reutilizables

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> noVacio = texto -> !texto.isBlank();
        Predicate<String> minimo8 = texto -> texto.length() >= 8;
        Predicate<String> contieneNumero = texto -> texto.matches(".*\\d.*");

        Predicate<String> passwordValida = noVacio.and(minimo8).and(contieneNumero);

        System.out.println(passwordValida.test("abc"));       // false
        System.out.println(passwordValida.test("abc12345"));  // true
    }
}

📌 Esto permite crear validaciones pequeñas y combinarlas.


🧪 Ejemplo práctico: procesar pedidos

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

class Pedido {
    private String cliente;
    private double total;
    private boolean pagado;

    public Pedido(String cliente, double total, boolean pagado) {
        this.cliente = cliente;
        this.total = total;
        this.pagado = pagado;
    }

    public String getCliente() {
        return cliente;
    }

    public double getTotal() {
        return total;
    }

    public boolean isPagado() {
        return pagado;
    }

    @Override
    public String toString() {
        return cliente + " - " + total + "€ - pagado: " + pagado;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Pedido> pedidos = new ArrayList<>();
        pedidos.add(new Pedido("Ana", 120, true));
        pedidos.add(new Pedido("Luis", 40, false));
        pedidos.add(new Pedido("Marta", 300, true));

        Predicate<Pedido> pedidoPagado = p -> p.isPagado();
        Predicate<Pedido> pedidoGrande = p -> p.getTotal() >= 100;
        Consumer<Pedido> imprimir = p -> System.out.println(p);

        pedidos.forEach(p -> {
            if (pedidoPagado.and(pedidoGrande).test(p)) {
                imprimir.accept(p);
            }
        });
    }
}

Salida:

Ana - 120.0€ - pagado: true
Marta - 300.0€ - pagado: true

⚠️ Errores típicos con lambdas

❌ Error 1: usar una interfaz con más de un método abstracto

interface Accion {
    void ejecutar();
    void detener();
}

Accion a = () -> System.out.println("Ejecutando"); // Error

No se puede porque Java no sabe si la lambda implementa ejecutar o detener.


❌ Error 2: olvidar los paréntesis cuando no hay parámetros

Runnable r = -> System.out.println("Hola"); // Error

Correcto:

Runnable r = () -> System.out.println("Hola");

❌ Error 3: poner return sin llaves

Function<String, Integer> longitud = texto -> return texto.length(); // Error

Correcto:

Function<String, Integer> longitud = texto -> texto.length();

O:

Function<String, Integer> longitud = texto -> {
    return texto.length();
};

❌ Error 4: no devolver valor cuando toca

Function<String, Integer> longitud = texto -> {
    texto.length(); // Error
};

Correcto:

Function<String, Integer> longitud = texto -> {
    return texto.length();
};

❌ Error 5: modificar una variable local externa

int total = 0;

Consumer<Integer> sumar = n -> total += n; // Error

Las variables locales externas usadas dentro de una lambda deben ser finales o efectivamente finales.


🧠 Tabla resumen de sintaxis

Caso Sintaxis Ejemplo
Sin parámetros () -> acción () -> System.out.println("Hola")
Un parámetro x -> acción x -> x * 2
Varios parámetros (a, b) -> acción (a, b) -> a + b
Una expresión x -> expresión x -> x.length()
Varias instrucciones x -> { instrucciones; } x -> { System.out.println(x); }
Con retorno largo x -> { return valor; } x -> { return x.length(); }

🧠 Tabla resumen de interfaces funcionales

Interfaz Método Ejemplo mental
Predicate<T> test(T t) ¿Cumple una condición?
Consumer<T> accept(T t) Haz algo con este dato
Supplier<T> get() Dame un dato
Function<T, R> apply(T t) Convierte este dato en otro
BiFunction<T, U, R> apply(T t, U u) Convierte dos datos en uno
UnaryOperator<T> apply(T t) Transforma un dato en otro igual
BinaryOperator<T> apply(T t1, T t2) Combina dos datos iguales
Runnable run() Ejecuta una acción sin parámetros
Comparator<T> compare(T a, T b) Ordena dos objetos

🧩 ¿Cuándo usar lambdas?

Usa lambdas cuando:

  • Una interfaz tiene un solo método abstracto.
  • Quieres pasar una acción como parámetro.
  • Quieres filtrar, transformar, ordenar o recorrer datos.
  • Quieres evitar clases anónimas innecesarias.
  • Estás usando colecciones, Comparator, forEach, removeIf o Streams.

No conviene usar lambdas cuando:

  • El código se vuelve difícil de leer.
  • La lambda tiene demasiadas líneas.
  • Hay mucha lógica compleja.
  • Necesitas reutilizar ese comportamiento en muchos sitios.

En esos casos, puede ser mejor crear un método normal.


🧠 Regla de oro

Una lambda responde a esta pregunta:

¿Qué hago con este dato?

Ejemplos:

nombre -> System.out.println(nombre)

¿Qué hago con este nombre? Lo imprimo.

numero -> numero % 2 == 0

¿Qué hago con este número? Compruebo si es par.

producto -> producto.getPrecio()

¿Qué hago con este producto? Obtengo su precio.

(a, b) -> a + b

¿Qué hago con estos dos números? Los sumo.


🧪 Mini ejercicios de comprensión

1. ¿Qué imprime?

Function<String, Integer> f = texto -> texto.length() * 2;
System.out.println(f.apply("Java"));
Solución
8
`"Java"` tiene 4 caracteres. La función devuelve `4 * 2`.

2. ¿Qué imprime?

Predicate<Integer> p = n -> n > 10;
System.out.println(p.test(8));
System.out.println(p.test(15));
Solución
false
true

3. Completa la lambda

Completa para que devuelva true si el texto empieza por A.

Predicate<String> empiezaPorA = texto -> ____________________;
Solución
Predicate<String> empiezaPorA = texto -> texto.startsWith("A");

4. Corrige el error

Function<String, Integer> longitud = texto -> return texto.length();
Solución
Function<String, Integer> longitud = texto -> texto.length();
O también:
Function<String, Integer> longitud = texto -> {
    return texto.length();
};

✅ Conclusión

Las expresiones lambda son una herramienta fundamental del Java moderno.

Permiten escribir comportamientos de forma compacta y son especialmente útiles para:

  • Recorrer colecciones.
  • Filtrar elementos.
  • Ordenar objetos.
  • Transformar datos.
  • Crear validaciones.
  • Trabajar con Streams.

La clave está en recordar esto:

Una lambda solo puede usarse cuando Java espera una interfaz funcional.

Y una interfaz funcional es aquella que tiene un único método abstracto.