Java Streams provide a powerful way to process data in a functional style, allowing developers to write concise and readable code. However, misusing Streams can lead to inefficient, buggy, or unmaintainable code. In this blog, we’ll explore developers' top five common mistakes when working with Java Streams and how to avoid them.
1. Modifying the Source During Stream Processing
One common mistake is changing a collection while using a stream to process it. Streams are meant to handle data without making changes to the original collection. If you modify the collection during processing, it can cause errors like a ConcurrentModificationException.
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
list.stream().forEach(x -> list.add(x + 1)); // Runtime exception
Problem :
Streams are not thread-safe, and modifying the underlying collection during processing disrupts its consistency.
How to avoid and solve:
If you need to transform the data, collect the results into a new collection instead of modifying the original one.
List<Integer> transformedList = list.stream()
.map(x -> x + 1)
.collect(Collectors.toList());
2. Assuming Streams Can Be Reused
Streams are single-use objects. Once consumed by a terminal operation (e.g., forEach(), collect()), they cannot be reused. Attempting to reuse a stream will throw an IllegalStateException.
Stream<String> stream = List.of("a", "b", "c").stream();
stream.forEach(System.out::println); // Works
stream.forEach(System.out::println); // Throws IllegalStateException
Problem:
This mistake often occurs when developers try to perform multiple operations on the same stream without realizing it has already been consumed.
How to avoid and solve:
If you need to process the data multiple times, create a new stream for each operation.
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println);
list.stream().forEach(System.out::println); // Create a new stream
3. Improper Handling of Parallel Streams
Parallel streams can help speed up processing, but if not used carefully, they can cause problems. For example, using them on small datasets or shared resources can lead to errors or slow things down instead of improving performance.
List<Integer> list = List.of(1, 2, 3);
list.parallelStream().forEach(System.out::println); // Non-deterministic output
Problem:
For small datasets, the overhead of parallelization often outweighs the performance gains.
Accessing shared mutable resources in parallel streams can lead to race conditions.
How to avoid and solve:
Use parallel streams only for large datasets where they provide a clear performance boost, and make sure to handle shared resources safely to avoid errors.
list.stream().forEach(System.out::println); // Sequential stream
4. Avoid Side Effects in Stream Operations
Streams are meant to follow a functional programming style. Adding side effects, like printing or changing external variables, in operations like map() or filter() goes against this approach and makes the code harder to maintain.
list.stream()
.map(x -> {
System.out.println(x); // Side effect
return x * 2;
})
.collect(Collectors.toList());
Problem:
Side effects can make the stream pipeline harder to understand and debug. They can also cause unexpected behaviour when using parallel streams.
How to avoid and solve:
If you need to debug or inspect values, use peek(). Avoid modifying external state in stream operations.
list.stream()
.peek(System.out::println) // Use peek for debugging
.map(x -> x * 2)
.collect(Collectors.toList());
5. Improper Collecting with Collectors.toMap
When using Collectors.toMap to collect data into a map, developers sometimes forget to handle key collisions. If two elements have the same key, the code will throw an IllegalStateException unless collisions are properly managed.
Map<Integer, String> map = List.of("apple", "bat", "ant").stream()
.collect(Collectors.toMap(String::length, Function.identity())); // Key collision issue
Problem:
If two elements produce the same key, the collector does not know how to merge them, leading to an exception.
How to avoid and solve:
Provide a merge function to handle key collisions.
Map<Integer, String> map = List.of("apple", "bat", "ant").stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(existing, replacement) -> existing // Resolve collision
));
Conclusion
Java Streams are a powerful tool for processing data, but using them incorrectly can cause inefficiency and errors. By being aware of these common mistakes and knowing how to avoid them, you can write clean, efficient, and maintainable code that makes the most of streams.
References :
https://www.baeldung.com/java-8-streams
https://www.digitalocean.com/community/tutorials/java-8-stream
https://docs.oracle.com/javase/8/docs/api/?java/util/stream/Stream.html
Top comments (1)
Very nice article, thank you! I didn’t know the peek() method, pretty useful