Saltar al contenido principal

Stream API

Un Stream no almacena datos. Describe un pipeline que consume una fuente —por ejemplo una colección— para filtrar, transformar u obtener un resultado.

Fuente, operaciones y resultado​

List<String> nombres = List.of("Ana", "Leo", "Marta");
List<String> largos = nombres.stream()
.filter(nombre -> nombre.length() > 3)
.map(String::toUpperCase)
.toList();
  • La fuente es nombres.
  • filter y map son operaciones intermedias: devuelven otro Stream.
  • toList es terminal: ejecuta el pipeline y produce el resultado.

En Java 17 Stream.toList() devuelve una lista no modificable. Si necesitas una lista que después vaya a cambiar, crea una colección mutable a partir del resultado:

List<String> mutables = new ArrayList<>(largos);

Operaciones habituales​

long cantidad = nombres.stream().filter(n -> n.length() > 3).count();
boolean existe = nombres.stream().anyMatch(n -> n.startsWith("A"));
String primero = nombres.stream().sorted().findFirst().orElse("Sin datos");
List<String> unicos = List.of("A", "B", "A").stream().distinct().toList();
  • sorted ordena el flujo.
  • distinct elimina duplicados usando equals/hashCode.
  • count, findFirst y anyMatch son terminales.

reduce básico​

int total = List.of(2, 3, 5).stream()
.reduce(0, (acumulado, numero) -> acumulado + numero);

El valor 0 es la identidad y la lambda combina el acumulado con cada elemento.

Pereza​

Las operaciones intermedias no recorren la fuente hasta que llega una terminal:

var pipeline = nombres.stream()
.filter(nombre -> {
System.out.println("Comprobando " + nombre);
return nombre.length() > 3;
});

System.out.println("Pipeline creado");
long resultado = pipeline.count();

Primero se imprime Pipeline creado; las comprobaciones aparecen al ejecutar count().

Un Stream queda consumido después de una terminal. Para otra consulta, crea un nuevo Stream desde la fuente.

Imperativo o Stream​

Un bucle es claro para mutaciones y lógica paso a paso. Un Stream suele ser útil para consultas y transformaciones encadenadas. No es «siempre mejor».

Evita modificar estado externo dentro de filter o map: complica el razonamiento y contradice la función de esas operaciones. map transforma; filter decide si un elemento continúa.

Ejemplo completo​

import java.util.List;

public class StreamsDemo {
public static void main(String[] args) {
List<String> datos = List.of(" java ", "SQL", "java", "Docker");

List<String> resultado = datos.stream()
.map(String::trim)
.map(String::toLowerCase)
.filter(texto -> texto.length() >= 4)
.distinct()
.sorted()
.toList();

System.out.println(resultado);
System.out.println(resultado.stream().anyMatch("java"::equals));
System.out.println(resultado.stream().findFirst().orElse("Sin datos"));
int letras = resultado.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(letras);
}
}

Errores habituales​

  • Creer que el Stream guarda elementos.
  • Olvidar la operación terminal.
  • Reutilizar un Stream consumido.
  • Intentar modificar la lista de toList().
  • Introducir efectos secundarios en map o filter.
  • Usar Stream para ocultar una operación imperativa más sencilla.

Practica: cursos publicados​

Enunciado y solución​

Normaliza nombres, descarta vacíos, elimina duplicados, ordena y cuenta los resultados. Usa una nueva fuente para cada terminal.

import java.util.List;

public class CursosStreamDemo {
public static void main(String[] args) {
List<String> cursos = List.of(" Java ", "", "Docker", "java");
List<String> limpios = cursos.stream()
.map(String::trim)
.filter(texto -> !texto.isEmpty())
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();
System.out.println(limpios);
System.out.println(limpios.stream().count());
}
}

El pipeline expresa una consulta sobre la colección; la fuente original no se modifica.