- Expresiones Lambda
1.1 Clases anónimas como predecesoras
En versiones anteriores de Java, las clases anónimas eran el principal mecanismo para crear objetos funcionales. Estas clases carecen de nombre y se instancian directamente con new. Una característica clave es su sintaxis dual, que combina definición de clase y creación de objeto.
Las clases anónimas típicamente se usan una sola vez y residen solo en el heap, sin referencia en la pila, lo que facilita su recolección por el GC.
public class EjemploClaseAnonima {
public static void main(String[] args) {
ejecutarAccion(new Accion() {
@Override
public void realizar() {
System.out.println("Acción de dibujo ejecutada.");
}
});
ejecutarAccion(new Dibujo());
}
public static void ejecutarAccion(Accion accion) {
accion.realizar();
}
}
interface Accion {
void realizar();
}
class Dibujo implements Accion {
@Override
public void realizar() {
System.out.println("Dibujando una imagen.");
}
}
1.2 Introducción de las expresiones Lambda
Java 8 formalizó el concepto de interfaces con un único método abstracto, denominadas interfaces funcionales. Las expresiones Lambda permiten instanciar estas interfaces de manera concisa, reemplazando a las clases anónimas en contextos como el procesamiento de colecciones.
El núcleo de una Lambda es proporcionar una azúcar sintáctica para implementar el método abstracto de una interfaz funcional.
1.3 Sintaxis y condiciones de uso
La sintaxis general es:
(parametros) -> expresión
o
(parametros) -> { sentencias; }
Características clave:
- Declaración de tipos opcional: el compilador puede inferirlos.
- Paréntesis opcionales para un solo parámetro.
- Llaves opcionales para una sola sentencia.
- Palabra clave
returnimplícita para expresiones simples.
La interfaz debe ser funcional, es decir, tener un solo método abstracto. La anotación @FunctionalInterface verifica esto a nivel de compilador.
1.4 Comparación con clases anónimas
@FunctionalInterface
interface Proceso {
void ejecutar();
}
// Implementación tradicional
class Tarea implements Proceso {
@Override
public void ejecutar() {
System.out.println("Procesando tarea...");
}
}
new Tarea().ejecutar();
// Con clase anónima
new Proceso() {
@Override
public void ejecutar() {
System.out.println("Ejecutando proceso anónimo.");
}
}.ejecutar();
// Con Lambda
Proceso procesoLambda = () -> System.out.println("Operación Lambda.");
procesoLambda.ejecutar();
1.5 Ejemplo práctico: ordenamiento
import java.util.*;
import java.util.stream.Collectors;
public class EjemploOrdenamiento {
public static void main(String[] args) {
List<double> valores = new ArrayList<>(Arrays.asList(3.14, 2.71, 1.62, 4.56));
System.out.println("Lista original: " + valores);
// Ordenamiento con clase anónima
Collections.sort(valores, new Comparator<double>() {
@Override
public int compare(Double a, Double b) {
return Double.compare(b, a);
}
});
System.out.println("Orden descendente:");
valores.forEach(v -> System.out.println(v));
// Ordenamiento con Lambda
Collections.sort(valores, (a, b) -> Double.compare(a, b));
System.out.println("Orden ascendente:");
valores.forEach(System.out::println);
}
}</double></double>
- Referencias a métodos
Las referencias a métodos simplifican las expresiones Lambda que solo invocan un método existente. Se aplican cuando la Lambda es de la forma parametros -> Clase.metodo(parametros) o similar.
2.1 Método estático de una clase
interface Calculo {
double obtener();
}
class MatUtil {
static double piAprox() {
return 3.14159;
}
}
// Lambda
Calculo calculoLambda = () -> MatUtil.piAprox();
// Referencia a método
Calculo calculoRef = MatUtil::piAprox;
System.out.println("Valor de pi: " + calculoRef.obtener());
2.2 Método de un parámetro de instancia
interface Transformador {
String convertir(Recurso recurso, int nivel);
}
class Recurso {
String nombre;
String obtenerNombre(int n) {
return nombre + "_" + n;
}
}
// Lambda
Transformador tLambda = (r, n) -> r.obtenerNombre(n);
// Referencia a método
Transformador tRef = Recurso::obtenerNombre;
Recurso res = new Recurso();
res.nombre = "archivo";
System.out.println("Nombre transformado: " + tRef.convertir(res, 5));
2.3 Método de una instancia existente
interface Validador {
boolean comprobar(String cadena);
}
class Texto {
boolean esValido(String s) {
return s.length() > 0;
}
}
Texto txt = new Texto();
// Lambda
Validador vLambda = s -> txt.esValido(s);
// Referencia a método
Validador vRef = txt::esValido;
System.out.println("Cadena válida: " + vRef.comprobar("test"));
2.4 Constructor de una clase
interface Fabrica {
Elemento crear(String tipo, double valor);
}
class Elemento {
String tipo;
double valor;
Elemento(String t, double v) {
this.tipo = t;
this.valor = v;
}
}
// Lambda
Fabrica fLambda = (t, v) -> new Elemento(t, v);
// Referencia a constructor
Fabrica fRef = Elemento::new;
Elemento elem = fRef.crear("metal", 9.81);
System.out.println("Elemento: " + elem.tipo + ", " + elem.valor);
- Interfaces funcionales
Java 8 introduce el paquete java.util.function con interfaces funcionales predefinidas. Algunas comunes:
import java.util.function.*;
// Supplier: no recibe parámetros, retorna un valor
Supplier<string> proveedor = () -> "Suministrado";
System.out.println(proveedor.get());
// Consumer: recibe un parámetro, no retorna valor
Consumer<integer> consumidor = n -> System.out.println("Procesado: " + n);
consumidor.accept(42);
// Predicate: recibe un parámetro, retorna boolean
Predicate<string> predicado = s -> s.startsWith("A");
System.out.println("Empieza con A: " + predicado.test("Algoritmo"));
// BiFunction: recibe dos parámetros, retorna un valor
BiFunction<string double="" registro=""> biFuncion = (nom, val) -> new Registro(nom, val);
Registro reg = biFuncion.apply("dato", 3.14);</string></string></integer></string>
- API de Stream
El API Stream permite procesar colecciones de datos de manera declarativa medinate pipelines con operaciones intermedias y terminales.
4.1 Creación de streams
List<persona> personas = Arrays.asList(
new Persona("Ana", 28),
new Persona("Carlos", 35),
new Persona("Elena", 22)
);
Stream<persona> stream1 = personas.stream();
Stream<integer> stream2 = Stream.of(10, 20, 30);</integer></persona></persona>
4.2 Restricciones
Un stream solo se puede operar una vez. Las operaciones intermedias son perezosas y no se ejecutan sin una operación terminal.
4.3 Operaciones comunes
// Filtrado y transformación
personas.stream()
.filter(p -> p.edad > 25)
.map(p -> p.nombre.toUpperCase())
.forEach(System.out::println);
// Saltar y limitar elementos
personas.stream()
.skip(1)
.limit(2)
.collect(Collectors.toList());
// Operaciones de coincidencia
boolean todosMayores = personas.stream().allMatch(p -> p.edad > 18);
boolean algunJoven = personas.stream().anyMatch(p -> p.edad < 30);
boolean ningunoMayor60 = personas.stream().noneMatch(p -> p.edad > 60);
4.4 Reducción y recolección
// Reducción
int suma = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// Recolección a lista
List<string> nombres = personas.stream()
.map(p -> p.nombre)
.collect(Collectors.toList());
// Recolección a arreglo
String[] arregloNombres = personas.stream()
.map(p -> p.nombre)
.toArray(String[]::new);
// Agrupación
Map<string list="">> grupos = personas.stream()
.collect(Collectors.groupingBy(p -> p.edad > 30 ? "mayores" : "menores"));</string></string>
- Métodos default y static en interfaces
Java 8 permite definir métodos con implementación en interfaces usando default y static, ofreciendo mayor flexibilidad.
5.1 Sintaxis básica
interface Dispositivo {
String encender();
default String reiniciar() {
return "Reinicio estándar";
}
static String voltaje() {
return "220V";
}
}
5.2 Uso y resolución de conflictos
Si una clase implementa múltiples interfaces con métodos default idénticos, debe proporcionar una implementación explícita. La herencia de clase tiene prioridad sobre los métodos default de interfaces.
interface Motor {
default String potencia() {
return "100HP";
}
}
interface Bateria {
default String potencia() {
return "12V";
}
}
class Vehiculo implements Motor, Bateria {
@Override
public String potencia() {
return Motor.super.potencia(); // Selección explícita
}
}