1. Error: reutilización del stream
Un stream (Stream) en Java es una herramienta de un solo uso. En cuanto ejecutas una operación terminal (por ejemplo, collect(), forEach(), count()), el stream se cierra y deja de ser apto para usarse de nuevo.
Ejemplo de error
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // Todo bien
stream.forEach(System.out::println); // ¡Lanzará IllegalStateException!
Salida:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
Cómo evitarlo
Si necesitas realizar varias operaciones, crea un nuevo stream cada vez:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
Truco: Un stream es como un vaso desechable del comedor: lo usas y lo tiras; coge uno nuevo para la siguiente bebida.
2. Error: modificar la colección durante el stream
Si modificas la colección de origen mientras la recorres con un stream, obtendrás ConcurrentModificationException. Es un dolor frecuente al filtrar y eliminar elementos.
Ejemplo de error
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // ¡BOOM!
}
});
Resultado:
Exception in thread "main" java.util.ConcurrentModificationException
Cómo hacerlo bien
Usa métodos que no modifiquen la colección durante la iteración, o recopila el resultado en una colección nueva:
names.removeIf(name -> name.startsWith("A")); // OK, removeIf es seguro
// O con la Stream API:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
Recuerda: A los streams les gusta la pureza: no toques la colección de origen mientras la «consumen» los streams.
3. Error: olvidar limit en streams infinitos
Los métodos Stream.iterate y Stream.generate pueden crear streams infinitos. Si olvidas limitar su tamaño con limit, puedes quedarte colgado para siempre (o hasta un OutOfMemoryError).
Ejemplo de error
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // ¡El programa nunca terminará!
Cómo hacerlo bien
Limita el stream con limit:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Imprimirá los números del 0 al 9
Consejo: Los streams infinitos se parecen a una serie sin capítulo final: alguien tiene que parar, y mejor que seas tú.
4. Error: manejo incorrecto de null
La API de Stream no se lleva bien con null. Si en la colección aparece null, muchas operaciones pueden lanzar NullPointerException, especialmente si llamas a métodos sobre los elementos.
Ejemplo de error
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // ¡BOOM! en null
.forEach(System.out::println);
Resultado:
Exception in thread "main" java.lang.NullPointerException
Cómo hacerlo bien
Filtra los valores null de antemano:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Trabajo con Optional
Si tienes un Stream<Optional<T>>, usa flatMap(Optional::stream) (Java 9+) para «desenvolverlos»:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Imprimirá solo "A" y "B"
Consejo: Procura no añadir null a las colecciones. Si no hay más remedio, filtéralos al principio.
5. Error: pérdida del orden de los elementos
Algunas operaciones (por ejemplo, flatMap, concat) pueden afectar al orden de los elementos, especialmente si trabajas con streams paralelos o si unes distintas colecciones.
Ejemplo de error
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — el orden se mantiene
// Pero si usas streams paralelos:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // ¡El orden puede ser inesperado!
Cómo hacerlo bien
Si el orden importa, evita los streams paralelos o usa métodos que lo garanticen (forEachOrdered):
par.forEachOrdered(System.out::print);
Hecho: La API de Stream conserva el orden por defecto, salvo que tú mismo lo hayas «roto».
6. Error: unión ineficiente de colecciones
Para operaciones sobre conjuntos es mejor usar Set en lugar de List, para evitar duplicados y mejorar el rendimiento. También es ineficiente escribir bucles anidados cuando puedes usar flatMap.
Ejemplo de error
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// Ineficiente:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // contains en List — O(n)
union.add(s);
}
}
// Eficiente con Set y la Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
O para la diferencia:
// Ineficiente:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, pero si lo hicieras con stream y list.contains — sería lento
// Eficiente:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
Consejo: Para trabajar con elementos únicos, usa siempre Set. Para colecciones grandes, evita bucles anidados; prioriza stream + flatMap.
7. Error: reutilizar operaciones terminales
A veces apetece «dividir» un stream en dos ramas, por ejemplo, recopilar los elementos en una lista y a la vez contarlos. Pero un stream es de un solo uso y la segunda operación provocará un error.
Ejemplo de error
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // ¡BOOM! IllegalStateException
Cómo hacerlo bien
Si necesitas usar el stream varias veces, primero recopílalo en una colección:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
Consejo: Si quieres ambas cosas, primero recopila en una colección.
8. Error: olvidar close() en recursos
Si usas streams asociados a recursos (por ejemplo, Files.lines(Path)), no olvides cerrarlos tras su uso para evitar fugas.
Ejemplo de error
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, pero el recurso no está cerrado!
Cómo hacerlo bien
Usa try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
Consejo: Los streams de archivos son como una tetera: caliéntala y apágala.
9. Error: manejo incorrecto de Optional y null en flatMap
Al trabajar con colecciones del tipo List<Optional<T>> muchos usan map(Optional::get), lo que provoca excepciones si dentro de Optional no hay valor.
Ejemplo de error
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // ¡BOOM! NoSuchElementException
.forEach(System.out::println);
Cómo hacerlo bien
Usa flatMap(Optional::stream) (Java 9+):
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Solo "A"
Consejo: Optional no es solo un envoltorio, sino una herramienta para trabajar de forma segura con la ausencia de valor.
10. Error: equals/hashCode mal implementados
Las operaciones sobre conjuntos (Set, distinct, unión, intersección) dependen de una implementación correcta de los métodos equals y hashCode.
Ejemplo de error
class Student {
String name;
int age;
// ¡Sin equals ni hashCode!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, aunque deberían considerarse iguales.
Cómo hacerlo bien
Sobrescribe equals y hashCode en tus clases si planeas usarlas en colecciones Set, Map o para operaciones distinct.
11. Error: usar el stream después de collect
Tras una operación terminal (por ejemplo, collect) el stream se cierra. Cualquier intento de continuar trabajando con ese mismo stream provocará un error.
Ejemplo de error
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // ¡Excepción!
Cómo hacerlo bien
Crea un nuevo stream a partir de la colección:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. Error: uso incorrecto de parallelStream
Muchos piensan que parallelStream() siempre acelera el procesamiento. En realidad, para colecciones pequeñas u operaciones simples, el paralelismo puede incluso ralentizar la ejecución debido al overhead de gestión de hilos.
Ejemplo de error
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Puede ser más lento que un stream() normal
Cómo hacerlo bien
Usa parallelStream solo para colecciones realmente grandes o cálculos costosos, especialmente si las operaciones son puras (sin efectos secundarios) y asociativas.
13. Error: operador terminal olvidado
Las operaciones intermedias (map, filter, flatMap, etc.) no se ejecutan hasta que se invoca un operador terminal (collect, forEach, count, etc.).
Ejemplo de error
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// ¡No ocurrirá nada!
Cómo hacerlo bien
Asegúrate de invocar un operador terminal:
stream.forEach(System.out::println);
GO TO FULL VERSION