Streams y Programación Funcional
¿Qué son los Streams?
Los Streams (Java 8+) permiten procesar colecciones de datos de forma declarativa y funcional. No son estructuras de datos, son flujos de elementos que se procesan.
List<Integer> numeros = List.of(1, 2, 3, 4, 5);
// Imperativo (antiguo)int suma = 0;for (int numero : numeros) { if (numero % 2 == 0) { suma += numero * 2; }}
// Declarativo con Streamsint suma = numeros.stream() .filter(n -> n % 2 == 0) .map(n -> n * 2) .reduce(0, Integer::sum);Crear Streams
Desde colecciones
List<String> lista = List.of("a", "b", "c");Stream<String> stream = lista.stream();Desde arrays
String[] array = {"a", "b", "c"};Stream<String> stream = Arrays.stream(array);Con Stream.of
Stream<String> stream = Stream.of("a", "b", "c");Streams infinitos
// Generar valoresStream<Integer> infinito = Stream.generate(() -> 42);
// IterarStream<Integer> numeros = Stream.iterate(0, n -> n + 1); // 0, 1, 2, 3...Streams numéricos
IntStream enteros = IntStream.range(1, 10); // 1 a 9IntStream enterosInclusivo = IntStream.rangeClosed(1, 10); // 1 a 10DoubleStream decimales = DoubleStream.of(1.1, 2.2, 3.3);Operaciones intermedias
Retornan un nuevo Stream. Son lazy (no se ejecutan hasta una operación terminal).
filter
Filtra elementos que cumplen una condición:
List<Integer> numeros = List.of(1, 2, 3, 4, 5, 6);
List<Integer> pares = numeros.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // [2, 4, 6]map
Transforma cada elemento:
List<String> nombres = List.of("ana", "luis", "maría");
List<String> mayusculas = nombres.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // ["ANA", "LUIS", "MARÍA"]flatMap
Aplana streams anidados:
List<List<Integer>> listas = List.of( List.of(1, 2), List.of(3, 4), List.of(5, 6));
List<Integer> plano = listas.stream() .flatMap(List::stream) .collect(Collectors.toList()); // [1, 2, 3, 4, 5, 6]distinct
Elimina duplicados:
List<Integer> numeros = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unicos = numeros.stream() .distinct() .collect(Collectors.toList()); // [1, 2, 3, 4]sorted
Ordena elementos:
List<Integer> numeros = List.of(5, 2, 8, 1, 9);
List<Integer> ordenados = numeros.stream() .sorted() .collect(Collectors.toList()); // [1, 2, 5, 8, 9]
// Con comparadorList<Integer> descendente = numeros.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); // [9, 8, 5, 2, 1]limit y skip
List<Integer> numeros = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> primeros = numeros.stream() .limit(5) .collect(Collectors.toList()); // [1, 2, 3, 4, 5]
List<Integer> sinPrimeros = numeros.stream() .skip(3) .collect(Collectors.toList()); // [4, 5, 6, 7, 8, 9, 10]peek
Para debugging, ejecuta una acción sin modificar el stream:
List<Integer> resultado = numeros.stream() .filter(n -> n % 2 == 0) .peek(n -> System.out.println("Filtrado: " + n)) .map(n -> n * 2) .peek(n -> System.out.println("Mapeado: " + n)) .collect(Collectors.toList());Operaciones terminales
Ejecutan el stream y retornan un resultado.
collect
Convierte el stream en una colección:
List<String> lista = stream.collect(Collectors.toList());Set<String> conjunto = stream.collect(Collectors.toSet());Map<String, Integer> mapa = stream.collect( Collectors.toMap(String::toLowerCase, String::length));forEach
Ejecuta una acción para cada elemento:
numeros.stream() .filter(n -> n % 2 == 0) .forEach(System.out::println);count
Cuenta elementos:
long cantidad = numeros.stream() .filter(n -> n > 5) .count();reduce
Combina elementos en un solo valor:
// Sumaint suma = numeros.stream() .reduce(0, (a, b) -> a + b);
// Con method referenceint suma = numeros.stream() .reduce(0, Integer::sum);
// Productoint producto = numeros.stream() .reduce(1, (a, b) -> a * b);
// Concatenar stringsString concatenado = nombres.stream() .reduce("", (a, b) -> a + b);Operaciones de búsqueda
// Encontrar cualquieraOptional<Integer> cualquiera = numeros.stream() .filter(n -> n > 5) .findAny();
// Encontrar el primeroOptional<Integer> primero = numeros.stream() .filter(n -> n > 5) .findFirst();
// Verificar si alguno cumpleboolean hayMayores = numeros.stream() .anyMatch(n -> n > 100);
// Verificar si todos cumplenboolean todosPares = numeros.stream() .allMatch(n -> n % 2 == 0);
// Verificar si ninguno cumpleboolean ningunNegativo = numeros.stream() .noneMatch(n -> n < 0);min y max
Optional<Integer> minimo = numeros.stream().min(Integer::compare);Optional<Integer> maximo = numeros.stream().max(Integer::compare);Collectors
Colecciones básicas
List<String> lista = stream.collect(Collectors.toList());Set<String> conjunto = stream.collect(Collectors.toSet());
// Java 16+List<String> lista = stream.toList(); // Inmutablejoining
Concatenar strings:
String resultado = List.of("Java", "Python", "JavaScript") .stream() .collect(Collectors.joining()); // "JavaPythonJavaScript"
String conSeparador = List.of("Java", "Python", "JavaScript") .stream() .collect(Collectors.joining(", ")); // "Java, Python, JavaScript"
String completo = List.of("Java", "Python", "JavaScript") .stream() .collect(Collectors.joining(", ", "[", "]")); // "[Java, Python, JavaScript]"Agrupamiento
class Persona { String nombre; int edad; String ciudad;}
List<Persona> personas = ...;
// Agrupar por ciudadMap<String, List<Persona>> porCiudad = personas.stream() .collect(Collectors.groupingBy(Persona::getCiudad));
// Contar por ciudadMap<String, Long> cantidadPorCiudad = personas.stream() .collect(Collectors.groupingBy( Persona::getCiudad, Collectors.counting() ));
// Agrupar y obtener nombresMap<String, List<String>> nombresPorCiudad = personas.stream() .collect(Collectors.groupingBy( Persona::getCiudad, Collectors.mapping(Persona::getNombre, Collectors.toList()) ));Particionamiento
Dividir en dos grupos (true/false):
Map<Boolean, List<Integer>> particion = numeros.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0));
List<Integer> pares = particion.get(true);List<Integer> impares = particion.get(false);Estadísticas
IntSummaryStatistics stats = numeros.stream() .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println("Cantidad: " + stats.getCount());System.out.println("Suma: " + stats.getSum());System.out.println("Promedio: " + stats.getAverage());System.out.println("Mínimo: " + stats.getMin());System.out.println("Máximo: " + stats.getMax());Optional
Para manejar valores que pueden o no existir:
Optional<String> opcional = Optional.of("valor");Optional<String> vacio = Optional.empty();Optional<String> nulleable = Optional.ofNullable(valor); // null-safe
// Verificar si existeif (opcional.isPresent()) { String valor = opcional.get();}
// Mejor forma (Java 8+)opcional.ifPresent(valor -> System.out.println(valor));
// Con valor por defectoString valor = opcional.orElse("default");String valor = opcional.orElseGet(() -> "default");String valor = opcional.orElseThrow(() -> new Exception("No existe"));
// TransformarOptional<Integer> longitud = opcional.map(String::length);
// FiltrarOptional<String> filtrado = opcional.filter(s -> s.length() > 5);
// Java 9+opcional.ifPresentOrElse( valor -> System.out.println(valor), () -> System.out.println("Vacío"));Lambdas y Method References
Lambdas
// Función con un parámetroFunction<String, Integer> longitud = s -> s.length();
// Función con múltiples parámetrosBiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;
// Con bloque de códigoFunction<String, String> procesar = s -> { String resultado = s.toUpperCase(); return resultado.trim();};
// Sin parámetrosSupplier<Integer> random = () -> (int) (Math.random() * 100);
// Sin retornoConsumer<String> imprimir = s -> System.out.println(s);Method References
// Método estáticoFunction<String, Integer> parse = Integer::parseInt;
// Método de instancia de un objetoString texto = "Hola";Supplier<Integer> longitud = texto::length;
// Método de instancia de una claseFunction<String, String> mayus = String::toUpperCase;
// ConstructorSupplier<ArrayList<String>> constructor = ArrayList::new;Function<Integer, ArrayList<String>> conCapacidad = ArrayList::new;Parallel Streams
Para procesamiento paralelo:
long suma = numeros.parallelStream() .filter(n -> n % 2 == 0) .mapToLong(Integer::longValue) .sum();Cuidado: Solo úsalos si:
- El dataset es grande (>10,000 elementos)
- Las operaciones son costosas
- Las operaciones son independientes (sin estado compartido)
Buenas prácticas
- No modifiques la fuente durante el stream:
// ❌ Mallista.stream().forEach(lista::remove); // ConcurrentModificationException
// ✅ BienList<String> nueva = lista.stream() .filter(condicion) .collect(Collectors.toList());- Evita efectos secundarios en operaciones intermedias:
// ❌ MalList<String> resultado = new ArrayList<>();stream.filter(condicion).forEach(resultado::add);
// ✅ BienList<String> resultado = stream .filter(condicion) .collect(Collectors.toList());- No reutilices streams:
Stream<String> stream = lista.stream();stream.forEach(System.out::println);stream.forEach(System.out::println); // ❌ IllegalStateException- Usa streams para operaciones complejas, no para todo:
// ❌ Excesivo para algo simplelista.stream().forEach(System.out::println);
// ✅ Más clarolista.forEach(System.out::println);Próximo paso
Aprende a organizar tu código en proyectos: Organización de código →