Las expresiones Lambda representan una característica fundamental introducida en Java 8, proporcionando una sintaxis concisa para implementar interfaces funcionales (aquellos que definen un único método abstracto). Permiten tratar el código como datos, habilitando la programación funcional en el lenguaje.
Requisitos de las interfaces para expresiones Lambda
Para que una interfaz sea compatible con una expresión Lambda, debe ser una interfaz funcional. Esto significa que la interfaz debe declarar exactamente un único método abstracto. La anotación @FunctionalInterface se puede usar para indicar explícitamente esta intención; el compilador emitirá un error si se declaran métodos abstractos adicionales. Los métodos default y static no cuentan como métodos abstractos.
Sintaxis fundamental y simplificaciones
La estructura básica de una Lambda consta de una lista de parámetros, el operador -> y un cuerpo de función.
// Interfaz funcional de ejemplo
@FunctionalInterface
interface Calculadora {
int calcular(int a, int b);
}
// Implementación con Lambda
Calculadora suma = (x, y) -> x + y;
int resultado = suma.calcular(5, 7); // 12
La sintaxis puede simplificarse bajo ciertas condiciones:
- Los tipos de los parámetros se pueden omitir si el compilador puede inferirlos. Si se omite el tipo de un parámetro, se deben omitir para todos.
- Si hay un solo parámetro, los paréntesis
()pueden omitirse. - Si el cuerpo consiste en una única instrucción, las llaves
{}pueden omitirse. - Si la instrucción única es una sentencia
return, la palabra clavereturntambién se omite junto con las llaves.
// Lambda simplificada
Calculadora multiplicacion = (a, b) -> a * b;
Referencia a métodos y constructores
Se pueden referenciar métodos existentes usando el operador ::.
class OperacionesMatematicas {
static int sumar(int a, int b) {
return a + b;
}
}
// Referencia a método estático
Calculadora calcRef = OperacionesMatematicas::sumar;
int total = calcRef.calcular(10, 20); // 30
Los constructores también pueden ser referenciados.
interface FabricaPersona {
Persona crear(String nombre, int edad);
}
class Persona {
String nombre;
int edad;
Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
}
// Referencia al constructor
FabricaPersona fabrica = Persona::new;
Persona nuevaPersona = fabrica.crear("Ana", 25);
Las expresiones Lambda no pueden acceder a métodos default de la interfza directamente.
Interfaces funcionales integradas
El paquete java.util.function proporciona una suite de interfaces funcionales genéricas predefinidas para escenarios comunes.
Predicate<T>: Recibe un argumento de tipoTy devuelve unboolean.Function<T, R>: Recibe un argumento de tipoTy devuelve un resultado de tipoR.Consumer<T>: Recibe un argumento de tipoTy no devuelve nada.Supplier<T>: No recibe argumentos y devuelve un resultado de tipoT.
Existen varientes especializadas para tipos primitivos (IntPredicate, DoubleFunction, etc.) para mejorar el rendimiento.
API Stream para procesamiento de colecciones
La API Stream facilita el procesamiento declarativo de colecciones de datos. Un flujo (Stream) representa una secuencia de elementos sobre los cuales se pueden realizar operaciones en cadena.
- Operaciones intermedias: Devuelven un nuevo flujo. Son perezosas (lazy), no se ejecutan hasta que se invoca una operación terminal.
- Operaciones terminales: Producen un resultado o un efecto secundario, cerrando el flujo.
Un flujo se crea a partir de una fuente, como una colección.
List<String> nombres = Arrays.asList("Luis", "Ana", "Pedro", "Laura", "Andrés");
Operaciones comunes
filter(Predicate): Operación intermedia que selecciona elementos que cumplen un predicado.map(Function): Operación intermedia que transforma cada elemento aplicando una función.sorted(): Operación intermedia que ordena los elementos.forEach(Consumer): Operación terminal que realiza una acción por cada elemento.collect(Collector): Operación terminal que acumula los elementos del flujo en una estructura de datos (ej.Collectors.toList()).count(): Operación terminal que cuenta el número de elementos.reduce(): Operación terminal que reduce los elementos a un único valor mediante una función asociativa.
// Filtrar, transformar y recolectar
List<String> nombresConA = nombres.stream()
.filter(nombre -> nombre.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
// Resultado: [ANA, ANDRÉS]
// Reducir a una suma
int totalCaracteres = nombres.stream()
.mapToInt(String::length)
.sum();
Las operaciones de flujo se pueden ejecutar en secuencial o en paralelo. La ejecución paralela se activa usando parallelStream() o stream().parallel(), aprovechando múltiples núcleos de CPU.
// Ejecución paralela
long conteo = nombres.parallelStream()
.filter(nombre -> nombre.length() > 4)
.count();
Variables locales efectivamente finales
Las expresiones Lambda pueden capturar variables del ámbito local donde se definen, pero dichas variables deben ser efectivamente finales. Esto significa que su valor no puede cambiar después de la asignación inicial. El compilador impone esta restricción para garantizar la seguridad de hilos.
int valorExterno = 10;
Consumer<Integer> consumidor = num -> {
System.out.println(valorExterno); // Permitido: lectura
// valorExterno++; // Error de compilación: no se puede modificar
};