Dominando Expresiones Lambda en Java: Desde Básicos hasta Stream API
Dominando Expresiones Lambda en Java
Explora las expresiones lambda de Java, interfaces funcionales, genéricos y la Stream API con ejemplos claros.
Java 8 introdujo las expresiones lambda para mejorar el poder expresivo del lenguaje, permitiendo a los desarrolladores escribir código más conciso y legible.
Antes de sumergirte en lambdas, es esencial entender las interfaces funcionales, la columna vertebral de la implementación lambda.
¿Qué es una Interfaz Funcional?
Una interfaz funcional es una interfaz de Java que contiene exactamente un método abstracto. Este único método define el propósito previsto de la interfaz.
La interfaz Runnable de java.lang es un ejemplo clásico, exponiendo solo el método run().
Ejemplo 1: Definiendo una Interfaz Funcional
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MiInterfaz {
// Único método abstracto
double obtenerValor();
}
Aquí, MiInterfaz está anotada con @FunctionalInterface, lo que obliga al compilador a cumplir el contrato de interfaz funcional, previniendo adiciones accidentales de múltiples métodos abstractos. La anotación es opcional pero altamente recomendada para claridad y seguridad.
En Java 7, las interfaces funcionales se referían como Métodos Abstractos Únicos (SAMs) y se implementaban típicamente usando clases anónimas.
Ejemplo 2: Implementando un SAM con Clase Anónima
public class PruebaInterfazFuncional {
public static void main(String[] args) {
// Clase anónima implementando Runnable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Acabo de implementar la Interfaz Funcional Runnable.");
}
}).start();
}
}
Salida:
Acabo de implementar la Interfaz Funcional Runnable.
Aunque las clases anónimas reducen el código repetitivo comparado con implementaciones tradicionales, la sintaxis permanece verbosa. Las expresiones lambda eliminan la necesidad de nombres de métodos explícitos al pasar interfaces funcionales como argumentos.
Introducción a las Expresiones Lambda
Una expresión lambda es esencialmente un método anónimo que proporciona una implementación concisa del método abstracto de una interfaz funcional. No se ejecuta independientemente; en su lugar, suministra el comportamiento para el método de la interfaz.
Cómo Definir una Expresión Lambda en Java
La sintaxis general es:
(lista de parámetros) -> cuerpo lambda
El operador flecha (->) separa la lista de parámetros del cuerpo. A continuación se muestran ejemplos concretos.
Supongamos que tenemos un método que retorna Pi:
double obtenerValorPi() {
return 3.1415;
}
Usando una lambda:
() -> 3.1415
Sin parámetros, el lado izquierdo de la flecha está vacío, y el lado derecho es el cuerpo de la expresión que retorna el valor.
Tipos de Cuerpos Lambda
Los cuerpos lambda de Java vienen en dos formas:
1. Cuerpo de Expresión – contiene una sola expresión.
() -> System.out.println("Las lambdas son geniales");
2. Cuerpo de Bloque – encapsulado en llaves, permitiendo múltiples declaraciones y un return opcional.
() -> {
double pi = 3.1415;
return pi;
};
Nota: Los cuerpos de expresión no requieren return explícito, mientras que los cuerpos de bloque sí si se retorna un valor.
Ejemplo 3: Expresión Lambda en Práctica
Escribamos un programa Java que retorne Pi usando una expresión lambda.
import java.lang.FunctionalInterface;
@FunctionalInterface
interface MiInterfaz {
double obtenerValorPi();
}
public class Main {
public static void main(String[] args) {
MiInterfaz ref = () -> 3.1415;
System.out.println("Valor de Pi = " + ref.obtenerValorPi());
}
}
Salida:
Valor de Pi = 3.1415
Conclusiones clave:
MiInterfazdefine un único método abstracto.- Se declara una referencia a
MiInterfazpero no se instancia directamente. - La expresión lambda suministra la implementación para
obtenerValorPi(). - Invocar
ref.obtenerValorPi()dispara el cuerpo lambda.
Expresiones Lambda con Parámetros
Como los métodos, las lambdas pueden aceptar parámetros. Ejemplo:
(n) -> (n % 2) == 0
Esta lambda verifica si el entero de entrada es par.
Ejemplo 4: Invirtiendo una Cadena con Lambda Parametrizada
@FunctionalInterface
interface MiInterfaz {
String invertir(String s);
}
public class Main {
public static void main(String[] args) {
MiInterfaz ref = (str) -> {
String resultado = "";
for (int i = str.length() - 1; i >= 0; i--) {
resultado += str.charAt(i);
}
return resultado;
};
System.out.println("Lambda invertida = " + ref.invertir("Lambda"));
}
}
Salida:
Lambda invertida = adbmaL
Interfaz Funcional Genérica
Las interfaces funcionales pueden ser genéricas, permitiéndoles operar en cualquier tipo de dato. Por ejemplo:
@FunctionalInterface
interface MiInterfaz<T> {
T func(T t);
}
Ejemplo 5: Lambdas Genéricas para Strings e Integers
public class Main {
public static void main(String[] args) {
// Inversión de String
MiInterfaz<String> invertir = (str) -> {
String resultado = "";
for (int i = str.length() - 1; i >= 0; i--) {
resultado += str.charAt(i);
}
return resultado;
};
System.out.println("Lambda invertida = " + invertir.func("Lambda"));
// Factorial de Integer
MiInterfaz<Integer> factorial = (n) -> {
int resultado = 1;
for (int i = 1; i <= n; i++) {
resultado *= i;
}
return resultado;
};
System.out.println("factorial de 5 = " + factorial.func(5));
}
}
Salida:
Lambda invertida = adbmaL factorial de 5 = 120
Expresiones Lambda y la Stream API
El paquete java.util.stream, introducido en JDK 8, permite a los desarrolladores realizar operaciones de estilo funcional en colecciones: filtrado, mapeo, reducción, ordenación y más.
Considera una lista de nombres de lugares, cada uno prefijado por su país. Usando streams y lambdas, podemos extraer y manipular datos eficientemente.
Ejemplo 6: Procesamiento de Streams con Lambdas
import java.util.ArrayList;
import java.util.List;
public class StreamMain {
static List<String> lugares = new ArrayList<>();
public static List<String> obtenerLugares() {
lugares.add("Nepal, Katmandú");
lugares.add("Nepal, Pokhara");
lugares.add("India, Delhi");
lugares.add("EE.UU., Nueva York");
lugares.add("África, Nigeria");
return lugares;
}
public static void main(String[] args) {
List<String> misLugares = obtenerLugares();
System.out.println("Lugares de Nepal:");
misLugares.stream()
.filter(p -> p.startsWith("Nepal"))
.map(p -> p.toUpperCase())
.sorted()
.forEach(p -> System.out.println(p));
}
}
Salida:
Lugares de Nepal: NEPAL, KATMANDÚ NEPAL, POKHARA
Aquí, la cadena de operaciones de stream—filter, map, sorted, forEach—cada una acepta una lambda, reduciendo drásticamente el código repetitivo comparado con soluciones pre-Java-8.
Java
- Dominando las Interfaces en C#: Definición, Implementación y Casos de Uso Prácticos
- Dominar Operadores en Java: Tipos, Sintaxis y Ejemplos Prácticos
- Entender Expresiones, Sentencias y Bloques en Java
- Dominando interfaces en Java: Conceptos, implementación y mejores prácticas
- Dominando Try‑with‑Resources en Java: Gestión automática de recursos explicada
- Anotaciones en Java explicadas: Tipos, ubicación y ejemplos prácticos
- Interfaz vs Clase Abstracta en Java: Cómo elegir la abstracción correcta
- Expresiones Regulares en Java: Dominando la Coincidencia de Patrones con java.util.regex
- Entendiendo las Interfaces en Java: Definiciones, Usos y Buenas Prácticas
- Java 9 Métodos Privados en Interfaces: Mejorar Encapsulamiento y Reutilización de Código
- Dominar Expresiones Lambda en Java 8: Guía de Programación Funcional