1. Error: reusing a stream
A stream (Stream) in Java is a single-use tool. Once you perform a terminal operation (for example, collect(), forEach(), count()), the stream closes and becomes unusable for further processing.
Example of the mistake
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // All good
stream.forEach(System.out::println); // Will throw IllegalStateException!
Output:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
How to avoid
If you need to perform several operations, create a new stream each time:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
Pro tip: A stream is like a disposable cup in a cafeteria: use it once and throw it away—grab a new one for the next drink.
2. Error: modifying a collection during a stream
If you modify the source collection while traversing it via a stream, you’ll get a ConcurrentModificationException. This is a common pain point when filtering and removing elements.
Example of the mistake
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // BOOM!
}
});
Result:
Exception in thread "main" java.util.ConcurrentModificationException
The right way
Use methods that do not modify the collection during traversal, or collect the result into a new collection:
names.removeIf(name -> name.startsWith("A")); // OK, removeIf is safe
// Or using the Stream API:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
Remember: Streams like cleanliness—don’t touch the source collection while streams are "consuming" it.
3. Error: forgetting limit with infinite streams
The methods Stream.iterate and Stream.generate can create infinite streams. If you forget to limit their size with limit, you can hang forever (or until an OutOfMemoryError).
Example of the mistake
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // The program will never terminate!
The right way
Limit the stream with limit:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Prints numbers 0 through 9
Tip: Infinite streams are like a TV series with no finale: someone has to stop it—and it should be you!
4. Error: improper handling of null
The Stream API doesn’t like null. If a collection contains null, many operations can throw a NullPointerException, especially when you call methods on elements.
Example of the mistake
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // BOOM! On null
.forEach(System.out::println);
Result:
Exception in thread "main" java.lang.NullPointerException
The right way
Filter out nulls upfront:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Working with Optional
If you have a Stream<Optional<T>>, use flatMap(Optional::stream) (Java 9+) to “unwrap” it:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Will print only "A" and "B"
Tip: Try not to add null to collections. If you can’t avoid them, filter them at the start.
5. Error: losing element order
Some operations (for example, flatMap, concat) can affect element order, especially if you work with parallel streams or merge different collections.
Example of the mistake
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 - order is preserved
// But if you use parallel streams:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // Order may be unexpected!
The right way
If order matters, avoid parallel streams or use methods that guarantee order (forEachOrdered):
par.forEachOrdered(System.out::print);
Fact: The Stream API preserves order by default unless you’ve “broken” it yourself.
6. Error: inefficient merging of collections
For set operations, prefer Set over List to avoid duplicates and improve performance. It’s also inefficient to write nested loops where you can use flatMap.
Example of the mistake
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// Inefficient:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // List.contains is O(n)
union.add(s);
}
}
// Efficient with Set and the Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
Or for difference:
// Inefficient:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, but if you implemented it via stream and list.contains, it would be slow
// Efficient:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
Tip: For unique elements, always use a Set. For large collections, avoid nested loops; prefer stream + flatMap.
7. Error: using multiple terminal operations on the same stream
Sometimes you want to “split” a stream into two branches, for example, collect elements into a list and simultaneously count them. But a stream is single-use, and the second operation will fail.
Example of the mistake
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // BOOM! IllegalStateException
The right way
If you need to use a stream multiple times, first collect it into a collection:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
Tip: If you want to have your cake and eat it too—collect into a collection first.
8. Error: forgetting close() on resources
If you use streams tied to resources (for example, Files.lines(Path)), don’t forget to close them after use to avoid leaks.
Example of the mistake
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, but the resource is not closed!
The right way
Use try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
Tip: File-backed streams are like a kettle: once it boils—turn it off!
9. Error: incorrect handling of Optional and null in flatMap
When working with collections like List<Optional<T>>, many use map(Optional::get), which leads to exceptions if the Optional is empty.
Example of the mistake
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // BOOM! NoSuchElementException
.forEach(System.out::println);
The right way
Use flatMap(Optional::stream) (Java 9+):
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Only "A"
Tip: Optional is not just a wrapper; it’s a tool for safe handling of absence.
10. Error: incorrectly implemented equals/hashCode
Set operations (Set, distinct, union, intersection) depend on correct implementations of equals and hashCode.
Example of the mistake
class Student {
String name;
int age;
// No equals and hashCode!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, although they should be equal!
The right way
Override equals and hashCode for your classes if you plan to use them in Set, Map, or for distinct operations.
11. Error: using a stream after collect
After a terminal operation (for example, collect) the stream is closed. Any attempt to continue working with that same stream will result in an error.
Example of the mistake
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); // Exception!
The right way
Create a new stream from the collection:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. Error: incorrect use of parallelStream
Many people think that parallelStream() always speeds up processing. In reality, for small collections or simple operations, parallelism can actually slow things down due to thread-management overhead.
Example of the mistake
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Might be slower than a regular stream()
The right way
Use parallelStream only for truly large collections or heavy computations, especially when operations are pure (side-effect free) and associative.
13. Error: forgotten terminal operator
Intermediate operations (map, filter, flatMap, etc.) do not run until a terminal operator is called (collect, forEach, count, etc.).
Example of the mistake
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Nothing will happen!
The right way
Be sure to call a terminal operator:
stream.forEach(System.out::println);
GO TO FULL VERSION