DEV Community

Cover image for Top five Common mistakes Developers make when using Java Streams and How to Avoid Them
Tharindu Dulshan Fernando
Tharindu Dulshan Fernando

Posted on

Top five Common mistakes Developers make when using Java Streams and How to Avoid Them

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
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
    ));

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
igventurelli profile image
Igor Venturelli

Very nice article, thank you! I didn’t know the peek() method, pretty useful