Introducción
Los Streams proporcionan un enfoque funcional para procesar colecciones de datos. Combinados con expresiones lambda, permiten código conciso y expresivo para filtrar, mapear y reducir datos.
Expresiones Lambda
Las lambdas son funciones anónimas que pueden tratarse como valores:
// Forma tradicional
Comparator<String> comparador = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
// Forma lambda
Comparator<String> lambdaComparador = (a, b) -> a.length() - b.length();
// Aún más simple con referencia de método
Comparator<String> refMetodo = Comparator.comparingInt(String::length);
Conceptos Básicos de Streams
Creando y usando streams:
List<String> nombres = Arrays.asList("Alicia", "Bob", "Carlos", "David");
// Filtrar y recolectar
List<String> nombresLargos = nombres.stream()
.filter(nombre -> nombre.length() > 4)
.collect(Collectors.toList());
// Resultado: [Alicia, Carlos, David]
// Transformación con map
List<Integer> longitudesNombres = nombres.stream()
.map(String::length)
.collect(Collectors.toList());
// Resultado: [6, 3, 6, 5]
// Contar elementos
long cantidad = nombres.stream()
.filter(nombre -> nombre.startsWith("A"))
.count();
// Resultado: 1
Operaciones Comunes de Stream
Filtrado:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> numerosPares = numeros.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Resultado: [2, 4, 6, 8, 10]
Mapeo:
List<String> mayusculas = nombres.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// Resultado: [ALICIA, BOB, CARLOS, DAVID]
Ordenamiento:
List<String> ordenados = nombres.stream()
.sorted()
.collect(Collectors.toList());
// Resultado: [Alicia, Bob, Carlos, David]
List<String> ordenadosPorLongitud = nombres.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// Resultado: [Bob, David, Alicia, Carlos]
Reducción:
// Sumar todos los números
int suma = numeros.stream()
.reduce(0, (a, b) -> a + b);
// Resultado: 55
// Alternativa con Integer::sum
int suma2 = numeros.stream()
.reduce(0, Integer::sum);
// Encontrar máximo
Optional<Integer> max = numeros.stream()
.max(Integer::compareTo);
FlatMap - Aplanar Colecciones Anidadas
List<List<Integer>> listaAnidada = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8)
);
List<Integer> aplanada = listaAnidada.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// Resultado: [1, 2, 3, 4, 5, 6, 7, 8]
Collectors
Agrupación:
List<String> palabras = Arrays.asList("manzana", "banana", "aguacate", "mora", "cereza");
Map<Character, List<String>> agrupadasPorPrimeraLetra = palabras.stream()
.collect(Collectors.groupingBy(palabra -> palabra.charAt(0)));
// Resultado: {m=[manzana, mora], b=[banana], a=[aguacate], c=[cereza]}
Particionamiento:
Map<Boolean, List<Integer>> particionados = numeros.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// Resultado: {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
Unión:
String unidos = nombres.stream()
.collect(Collectors.joining(", "));
// Resultado: "Alicia, Bob, Carlos, David"
Streams Paralelos
Procesa grandes conjuntos de datos en paralelo:
long cantidad = listaGrande.parallelStream()
.filter(item -> item.esValido())
.count();
Buenas Prácticas
- Usa streams para procesamiento declarativo de datos
- Prefiere referencias de métodos cuando sea posible
- No abuses de streams para operaciones simples
- Ten cuidado con streams paralelos (no siempre son más rápidos)
- Evita efectos secundarios en operaciones de stream
- Usa
Optionalpara manejar valores faltantes de forma segura
Malas Prácticas Comunes
- Modificar estado externo dentro de operaciones de stream
- Usar streams para todo (a veces los bucles son más claros)
- No considerar el rendimiento para grandes conjuntos de datos
- Olvidar operaciones terminales (el stream no se ejecutará)
Ejemplos Prácticos
Filtrar y transformar datos de jugadores:
List<Jugador> jugadoresActivos = jugadores.stream()
.filter(Jugador::estaEnLinea)
.filter(j -> j.getNivel() > 10)
.sorted(Comparator.comparingInt(Jugador::getPuntaje).reversed())
.collect(Collectors.toList());
Calcular estadísticas:
DoubleSummaryStatistics estadisticas = puntajes.stream()
.mapToDouble(Puntaje::getValor)
.summaryStatistics();
System.out.println("Promedio: " + estadisticas.getAverage());
System.out.println("Máximo: " + estadisticas.getMax());
System.out.println("Cantidad: " + estadisticas.getCount());
Filtrado y agrupación compleja:
Map<String, Long> jugadoresPorPais = jugadores.stream()
.filter(j -> j.getNivel() > 20)
.collect(Collectors.groupingBy(
Jugador::getPais,
Collectors.counting()
));
Conclusión
Los Streams y las expresiones lambda proporcionan una forma poderosa y expresiva de procesar colecciones. Reducen el código repetitivo y hacen tus intenciones más claras, conduciendo a un código más mantenible.
En el próximo capítulo, exploraremos operaciones de entrada/salida de archivos.