CodeGym /Courses /JAVA 25 SELF /Breakdown of common mistakes when working with streams

Breakdown of common mistakes when working with streams

JAVA 25 SELF
Level 32, Lesson 4
Available

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);
1
Task
JAVA 25 SELF, level 32, lesson 4
Locked
Dangerous database cleanup with possible "empty" entries 😼
Dangerous database cleanup with possible "empty" entries 😼
1
Task
JAVA 25 SELF, level 32, lesson 4
Locked
Magic one-time-use scroll of spells 📜
Magic one-time-use scroll of spells 📜
1
Survey/quiz
Unions and projections, level 32, lesson 4
Unavailable
Unions and projections
Stream API: unions and projections
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION