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. filterymapson operaciones intermedias: devuelven otro Stream.toListes 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();
sortedordena el flujo.distinctelimina duplicados usandoequals/hashCode.count,findFirstyanyMatchson 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
mapofilter. - 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.