⚡ 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:
- Parámetros
- Token de flecha
- 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
xy devuelvex * 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 escribereturn. - Con llaves
{}→ si hay que devolver algo, se escribereturn.
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
Stringy devuelve unInteger.
🔗 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,removeIfo 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
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();
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.