Introducción a las expresiones Lambda
Una expresión Lambda representa una función anónima que puede asignarse a una variable o pasarse como argumento. En Java, su uso se apoya en interfaces funcionales, es decir, aquellas que declaran un único método abstracto. Este enfoque permite escribir código más conciso y favorece el uso de la programación funcional dentro del lenguaje orientado a objetos.
Ventajas e inconvenientes
Ventajas:
- Reduce la verbosidad del código y acelera el desarrollo.
- Facilita el procesamiento paralelo con
parallelStream. - Optimiza operaciones como
computeIfAbsenten mapas. - Evita la generación de clases extra asociadas a clases anónimas.
Inconvenientes:
- Puede disminuir la legibilidad si se abusa de ellas.
- Depurar resulta más complicado.
- Restringen la modificación de variables externas dentro de ciertos contextos.
- En cálculos secuenciales pequeños, pueden ser menos eficientes que un bucle tradicional.
Sintaxis básica
(parámetros) -> { cuerpo }
// Si hay un único parámetro, se pueden omitir los paréntesis.
// Si el cuerpo es una sola instrucción, se pueden omitir las llaves y el return.
Procesamiento paralelo con streams
Java permite convertir un stream convencional en un stream parlaelo mediante parallelStream() o parallel(). Esto divide los datos en fragmentos que se procesan simultáneamenet en diferentes núcleos.
import java.util.List;
public class ProcesoParalelo {
public static void main(String[] args) {
List<integer> numeros = List.of(1, 2, 3, 4, 5, 6);
numeros.parallelStream().forEach(n -> System.out.println(n));
numeros.parallelStream().filter(n -> n > 2).forEach(System.out::println);
numeros.parallelStream().map(n -> n * 2).forEach(System.out::println);
Integer total = numeros.parallelStream().reduce(0, (a, b) -> a + b);
numeros.parallelStream().sorted().forEach(System.out::println);
}
}
</integer>
No siempre el paralelismo mejora el rendimiento; solo es recomendable cuando el procesamiento es intensivo en CPU.
Ejemplos prácticos
Recorrido con forEach
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RecorridoEjemplo {
public static void main(String[] args) {
List<integer> valores = new ArrayList<>();
Collections.addAll(valores, 1, 2, 3, 4, 5, 6, 7, 8, 9);
valores.forEach(v -> System.out.println(v));
valores.forEach(System.out::println);
valores.forEach(v -> {
if (v % 2 == 0) {
System.out.println(v);
}
});
}
}
</integer>
Reemplazo de clases aónimas
public class EjemploFuncional {
interface Conversor {
int convertir(String texto);
}
public static void main(String[] args) {
Conversor conClaseAnonima = new Conversor() {
@Override
public int convertir(String texto) {
return Integer.parseInt(texto);
}
};
Conversor conLambda = texto -> Integer.parseInt(texto);
System.out.println(conClaseAnonima.convertir("100"));
System.out.println(conLambda.convertir("200"));
}
}
Manejo de Optional
import java.util.Optional;
public class EjemploOptional {
public static void main(String[] args) {
Optional<string> mensaje = Optional.ofNullable("Hola Java");
mensaje.ifPresent(texto -> System.out.println(texto));
String resultado = mensaje.orElseGet(() -> "Valor por defecto");
System.out.println(resultado);
}
}
</string>
Suma paralela
import java.util.List;
public class CalculoParalelo {
public static void main(String[] args) {
List<string> cadenas = List.of("10", "20", "30", "40");
System.out.println(sumar(cadenas));
}
public static int sumar(List<string> cadenas) {
return cadenas.parallelStream()
.mapToInt(Integer::parseInt)
.sum();
}
}
</string></string>
Ordenación de colecciones
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Empleado {
private String nombre;
private int edad;
public Empleado(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}
public class OrdenacionEjemplo {
public static void main(String[] args) {
List<empleado> equipo = new ArrayList<>();
equipo.add(new Empleado("Ana", 30));
equipo.add(new Empleado("Luis", 25));
equipo.add(new Empleado("Marta", 35));
equipo.sort((e1, e2) -> e1.getEdad() - e2.getEdad());
equipo.sort(Comparator.comparingInt(Empleado::getEdad));
equipo.sort(Comparator.comparing(Empleado::getNombre).reversed());
List<empleado> ordenados = equipo.stream()
.sorted(Comparator.comparing(Empleado::getEdad))
.toList();
}
}
</empleado></empleado>
TreeSet con Comparator
import java.util.TreeSet;
class Persona {
String nombre;
int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
@Override
public String toString() {
return nombre + " (" + edad + ")";
}
}
public class EjemploTreeSet {
public static void main(String[] args) {
TreeSet<persona> conjunto = new TreeSet<>((p1, p2) -> Integer.compare(p2.edad, p1.edad));
conjunto.add(new Persona("Sofía", 28));
conjunto.add(new Persona("Pedro", 35));
conjunto.add(new Persona("Elena", 22));
System.out.println(conjunto);
}
}
</persona>
Eliminación condicional con removeIf
import java.util.ArrayList;
import java.util.List;
public class EliminacionEjemplo {
public static void main(String[] args) {
List<persona> personas = new ArrayList<>();
personas.add(new Persona("Carlos", 15));
personas.add(new Persona("Diana", 22));
personas.add(new Persona("Bruno", 12));
personas.removeIf(p -> p.edad >= 18);
System.out.println(personas);
}
}
</persona>
Agrupación y partición
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AgrupacionEjemplo {
public static void main(String[] args) {
List<string> palabras = List.of("java", "lambda", "stream", "api");
Map<integer list="">> porLongitud = palabras.stream()
.collect(Collectors.groupingBy(String::length));
Map<boolean list="">> porTamano = palabras.stream()
.collect(Collectors.partitioningBy(p -> p.length() > 3));
System.out.println(porLongitud);
System.out.println(porTamano);
}
}
</boolean></integer></string>
Creación de hilos
public class HiloLambda {
public static void main(String[] args) {
Thread hilo = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
});
hilo.start();
}
}
Uso del API Stream
filter
List<string> nombres = List.of("Ana", "Luis", "María", "Juan");
List<string> sinLuis = nombres.stream()
.filter(n -> !"Luis".equals(n))
.toList();
String encontrado = nombres.stream()
.filter(n -> "Luis".equals(n))
.findAny()
.orElse("No existe");
</string></string>
map
List<string> minusculas = List.of("java", "lambda", "stream");
List<string> mayusculas = minusculas.stream()
.map(String::toUpperCase)
.toList();
List<integer> enteros = List.of("1", "2", "3").stream()
.map(Integer::valueOf)
.toList();
List<integer> cuadrados = List.of(1, 2, 3, 4).stream()
.map(n -> n * n)
.toList();
</integer></integer></string></string>
sorted, max, min y distinct
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public class OperacionesStream {
public static void main(String[] args) {
List<string> lista = List.of("Ana", "Luis", "María", "Juan");
lista.stream().sorted().forEach(System.out::println);
int maximo = lista.stream()
.mapToInt(String::length)
.max()
.orElse(0);
int minimo = lista.stream()
.mapToInt(String::length)
.min()
.orElse(0);
String frase = "hola hola mundo mundo java";
List<string> unicas = Stream.of(frase.split(" "))
.filter(p -> !p.isEmpty())
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();
}
}
</string></string>
reduce
import java.util.List;
import java.util.stream.Stream;
public class ReduccionEjemplo {
public static void main(String[] args) {
String concatenado = Stream.of("A", "B", "C").reduce("", String::concat);
double minimo = Stream.of(-1.5, 2.3, 0.8).reduce(Double.MAX_VALUE, Double::min);
List<integer> numeros = List.of(1, 2, 3, 4);
int total = numeros.stream().reduce(0, (a, b) -> a + b);
int totalSinSemilla = Stream.of(1, 2, 3, 4).reduce(Integer::sum).orElse(0);
}
}
</integer>
Operaciones de coincidencia
import java.util.List;
class Producto {
int id;
public Producto(int id) { this.id = id; }
public int getId() { return id; }
}
public class CoincidenciaEjemplo {
public static void main(String[] args) {
List<producto> productos = List.of(new Producto(2), new Producto(5), new Producto(8));
boolean todos = productos.stream().allMatch(p -> p.getId() > 3);
boolean alguno = productos.stream().anyMatch(p -> p.getId() > 3);
boolean ninguno = productos.stream().noneMatch(p -> p.getId() > 10);
System.out.println(todos);
System.out.println(alguno);
System.out.println(ninguno);
}
}
</producto>
Streams especializados
Para trabajar con tipos primitivos sin incurrir en autoboxing, Java ofrece IntStream, LongStream y DoubleStream.
Creación y operaciones básicas
import java.util.Random;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public class StreamsPrimitivos {
public static void main(String[] args) {
IntStream.of(1, 2, 3, 4).forEach(System.out::println);
IntStream.range(1, 5).forEach(System.out::println);
IntStream.rangeClosed(1, 5).forEach(System.out::println);
int[] datos = {1, 2, 3};
IntStream streamArray = IntStream.of(datos);
new Random().ints(5, 0, 100).forEach(System.out::println);
}
}
Filtrado, mapeo y ordenación
IntStream.of(1, 2, 3, 4, 5)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
IntStream.of(1, 2, 3)
.mapToLong(n -> (long) n * 10)
.forEach(System.out::println);
IntStream.of(3, 1, 4, 2)
.sorted()
.forEach(System.out::println);
IntStream.of(1, 2, 2, 3, 3, 3)
.distinct()
.forEach(System.out::println);
Agregación y estadísticas
import java.util.IntSummaryStatistics;
import java.util.stream.IntStream;
public class EstadisticasEjemplo {
public static void main(String[] args) {
int suma = IntStream.of(1, 2, 3).sum();
int maximo = IntStream.of(1, 2, 3).max().orElse(0);
double promedio = IntStream.of(1, 2, 3).average().orElse(0);
IntSummaryStatistics stats = IntStream.of(1, 2, 3, 4).summaryStatistics();
System.out.println(stats.getSum());
System.out.println(stats.getCount());
System.out.println(stats.getAverage());
}
}
Reduce y paralelismo
int factorial = IntStream.rangeClosed(1, 5)
.reduce(1, (a, b) -> a * b);
long sumaParalela = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.sum();
Referencias a métodos
Las referencias a métodos son una forma abreviada de expresiones Lambda cuando el cuerpo de la lambda simplemente invoca un método existente.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
interface Parser {
int ejecutar(String texto);
}
public class ReferenciasMetodos {
public static void main(String[] args) {
Parser parser1 = s -> Integer.parseInt(s);
Parser parser2 = Integer::parseInt;
System.out.println(parser2.ejecutar("123"));
List<integer> valores = Arrays.asList(45, 12, 78, 3);
valores.sort(Integer::compare);
valores.forEach(System.out::println);
}
}
</integer>
El operador :: permite referirse a métodos estáticos, métodos de instancia y constructores, reduciendo aún más la sintaxis.