5 errores críticos que todos cometen con Java Streams (y cómo evitarlos)
Los Streams de Java son poderosos, pero también fáciles de usar mal. Aquí tienes cinco errores reales, por qué son peligrosos y cómo corregirlos con ejemplos claros y prácticos.
1) Usar forEach para efectos secundarios en lugar de terminales puros
Error: usar forEach para mutar estado externo (p. ej. añadir a una lista) rompe la intención declarativa y puede causar problemas en ejecuciones paralelas.
// MAL: efectos secundarios, no es seguro en paralelo y difícil de razonar
List<String> out = new ArrayList<>();
items.stream().forEach(i -> out.add(transform(i)));
Corrección: usa operaciones de reducción/colección apropiadas (collect).
// BIEN: sin efectos secundarios visibles, claro y seguro
List<String> out = items.stream()
.map(this::transform)
.collect(Collectors.toList());
Por qué: collect puede usar collectors optimizados y compatibles con paralelo; forEach debe reservarse para operaciones terminales que no cambian el flujo de datos externo.
2) Reutilizar un Stream (IllegalStateException)
Error: intentar usar el mismo objeto Stream más de una vez causa IllegalStateException.
// MAL: arroja IllegalStateException en la segunda terminal
Stream<String> s = items.stream().filter(...);
long c1 = s.count();
long c2 = s.count(); // ERROR
Corrección: crea nuevos streams o usa un supplier si necesitas recrearlo:
// BIEN: recrear el stream
long c1 = items.stream().filter(...).count();
long c2 = items.stream().filter(...).count();
// O usar Supplier<Stream<T>> para evitar duplicación de lógica
Supplier<Stream<String>> supplier = () -> items.stream().filter(...);
long c1 = supplier.get().count();
long c2 = supplier.get().count();
Por qué: los Streams son consumibles una sola vez. Prefiere reutilizar la fuente o encapsular la construcción del stream.
3) Mutación compartida dentro de parallel() → condición de carrera
Error: actualizar variables compartidas dentro de streams paralelos causa race conditions y resultados no deterministas.
// MAL: no es seguro en paralelo
int[] sum = new int[1];
items.parallelStream().forEach(x -> sum[0] += x);
Corrección: usa operaciones de reducción/Collectors que gestionan la concurrencia correctamente o tipos atómicos/design inmutables.
// BIEN: reducción por suma (thread-safe internamente)
int sum = items.parallelStream().mapToInt(Integer::intValue).sum();
// BIEN: Collector que suma objetos complejos correctamente
MyResult result = items.parallelStream()
.collect(MyResult::new, MyResult::accumulate, MyResult::combine);
Por qué: los collectors bien implementados manejan particionado, combinación y sincronización cuando es necesario. Evita mutar variables fuera del pipeline.
4) Ignorar versiones primitivas (boxing/unboxing)
Error: usar Stream<Integer> en cálculos numéricos frecuentes provoca boxing/unboxing y pérdida de rendimiento.
// MAL: boxing en cada operación
int sum = items.stream().map(i -> i.getValue()).reduce(0, Integer::sum);
Corrección: usa IntStream, LongStream o DoubleStream cuando trabajes con primitivos.
// BIEN: operaciones con IntStream, sin boxing
int sum = items.stream().mapToInt(i -> i.getValue()).sum();
// Si necesitas volver a objetos:
List<Integer> boxed = IntStream.range(0, n).boxed().collect(Collectors.toList());
Por qué: evitar boxing reduce GC y mejora rendimiento especialmente en colecciones grandes o en pipelines intensivos de CPU.
5) Usar Files.lines(...) sin cerrar el stream (fuga de recursos)
Error: Files.lines(path) abre un stream ligado a IO que debe cerrarse; no hacerlo deja archivos abiertos.
// MAL: posible fuga de descriptor de archivo
Stream<String> lines = Files.lines(path);
long count = lines.filter(...).count(); // lines no cerrado explícitamente
Corrección: usar try-with-resources para asegurar el cierre.
// BIEN: try-with-resources
try (Stream<String> lines = Files.lines(path)) {
long count = lines.filter(...).count();
}
Por qué: streams basados en IO implementan AutoCloseable. No cerrarlos puede agotar handles y causar problemas en producción.
Resumen práctico rápido
- Evita efectos secundarios: usa
collecty funciones puras. - No reutilices un Stream; vuelve a construirlo o usa un Supplier.
- En paralelo, evita mutación compartida; usa reduction/collectors adecuados.
- Prefiere IntStream/LongStream/DoubleStream para números.
- Cierra streams de IO con try-with-resources.
Consejo avanzado: cuando la performance sea crítica, benchmarkea con JMH y considera escribir un Collector personalizado que haga particionado eficiente para tu caso de uso. Como siguiente paso, toma uno de tus pipelines más lentos, cámbialo a IntStream (si aplica) y mide la diferencia con JMH.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación