Stream API

Procesamiento declarativo de colecciones (java.util.stream)

Qué es un Stream

Un Stream<T> es una secuencia de elementos sobre la que se declaran operaciones en cadena. No es una colección: no almacena datos ni los modifica. Es un pipeline de transformaciones que se ejecuta de forma perezosa (lazy) hasta que una operación terminal lo materializa.

Distinción importante: java.util.stream (Stream API de colecciones) es completamente distinto de java.io (I/O streams de bytes/caracteres).

import java.util.stream.Stream;

// Desde una colección
List<String> lista = List.of("Ana", "Luis", "Eva");
Stream<String> s1 = lista.stream();

// Desde valores literales
Stream<String> s2 = Stream.of("a", "b", "c");

// Stream infinito con límite
Stream<Integer> numeros = Stream.iterate(0, n -> n + 1).limit(10);

// Stream vacío
Stream<String> vacio = Stream.empty();

Operaciones intermedias

Devuelven otro Stream y son lazy: no se ejecutan hasta que aparece una operación terminal. Las más usadas: filter, map, sorted, distinct, limit, skip.

List<String> nombres = List.of("Ana", "Carlos", "Eva", "Alberto", "Beatriz");

// filter: descarta elementos que no cumplen el predicado
List<String> conA = nombres.stream()
    .filter(n -> n.startsWith("A"))
    .collect(Collectors.toList());
// ["Ana", "Alberto"]

// map: transforma cada elemento
List<Integer> longitudes = nombres.stream()
    .map(String::length)
    .collect(Collectors.toList());
// [3, 6, 3, 7, 7]

// sorted: ordena (usa Comparable o Comparator)
List<String> ordenados = nombres.stream()
    .sorted()
    .collect(Collectors.toList());

// distinct + limit
List<String> primerosSinDuplicados = nombres.stream()
    .distinct()
    .limit(3)
    .collect(Collectors.toList());

Operaciones terminales

Desencadenan la ejecución del pipeline y producen un resultado final: una colección, un escalar, un Optional o un efecto secundario. Un stream solo puede consumirse una vez.

// forEach: terminal con efecto secundario (no devuelve Stream)
nombres.stream().forEach(System.out::println);

// collect: materializa el Stream en una colección
List<String> lista = stream.collect(Collectors.toList());
Set<String> conjunto = stream.collect(Collectors.toSet());

// count
long total = nombres.stream().filter(n -> n.length() > 3).count();

// findFirst / findAny: devuelven Optional
Optional<String> primero = nombres.stream().filter(n -> n.startsWith("E")).findFirst();

// anyMatch / allMatch / noneMatch
boolean hayLargo = nombres.stream().anyMatch(n -> n.length() > 5);
boolean todosMayus = nombres.stream().allMatch(n -> n.equals(n.toUpperCase()));

// reduce: combina todos los elementos en uno
Optional<Integer> suma = Stream.of(1, 2, 3, 4).reduce(Integer::sum);
int sumaConIdentidad = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

Collectors avanzados

La clase Collectors proporciona implementaciones de Collector listas para usar: agrupación, particionado, conteo, concatenación de strings y construcción de mapas.

import java.util.stream.Collectors;

List<String> nombres = List.of("Ana", "Carlos", "Eva", "Alberto");

// joining: concatena en un String
String csv = nombres.stream().collect(Collectors.joining(", "));
// "Ana, Carlos, Eva, Alberto"

// groupingBy: agrupa en un Map
Map<Integer, List<String>> porLongitud = nombres.stream()
    .collect(Collectors.groupingBy(String::length));
// {3=[Ana, Eva], 6=[Carlos], 7=[Alberto]}

// counting por grupo
Map<Integer, Long> conteo = nombres.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

// toMap
Map<String, Integer> nombreALongitud = nombres.stream()
    .collect(Collectors.toMap(n -> n, String::length));

flatMap: aplanar streams anidados

map produce un Stream<Stream<T>> cuando la función devuelve un stream. flatMap lo aplana en un Stream<T> — equivale al concatMap de otros lenguajes.

// flatMap: "aplana" streams anidados
List<List<String>> anidado = List.of(
    List.of("a", "b"),
    List.of("c", "d"),
    List.of("e")
);

List<String> plano = anidado.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// ["a", "b", "c", "d", "e"]

// Caso real: todas las palabras de varias frases
List<String> frases = List.of("hola mundo", "java ocho");
List<String> palabras = frases.stream()
    .flatMap(f -> Arrays.stream(f.split(" ")))
    .collect(Collectors.toList());
// ["hola", "mundo", "java", "ocho"]
Regla de uso: los streams son ideales para transformar y filtrar datos de forma declarativa. Evita efectos secundarios dentro de operaciones intermedias — para eso existe forEach. No uses streams cuando un bucle simple es más legible.

Siguiente apartado → Optional: valores potencialmente ausentes sin null

Índice de la sección

Índice del curso