Explain Codes LogoExplain Codes Logo

Java Stream API - Best way to transform a list: map or forEach?

java
stream-engineering
best-practices
performance
Alex KataevbyAlex Kataev·Feb 27, 2025
TLDR

For object transformation and crafting a new list, use .map(). For applying a function in-place without returning any new collection, lean on .forEach().

// With .map(), create a new list of squares--no magic here, only maths! List<Integer> squares = nums.stream().map(n -> n * n).collect(Collectors.toList());
// With .forEach(), it's like a Jedi mind trick on the original list. nums.forEach(n -> nums.set(nums.indexOf(n), n * n));

Immutable transformation? .map() it is. Mutating the original list? You got .forEach().

Detailing the map function

.map() is your stream transformer, crafting new objects/stream. It's ideal for cases demanding principles like statelessness, immutability- the hallmark of functional programming.

  • Stateless processing: Ensure your .map() lambdas are stateless to boost parallel processing.
  • Sequence maintenance: .map() respects order, essential for parallel streams, where order often matters.
  • Collector flexibility: Want to collect into a different structure? Modify the Collector to toSet(), toMap(), or anything your heart desires.

The forEach function, and stateful operations

Folks often misuse .forEach() for state mutation- a no-no in Stream API's functional programming playbook. External state changes during .forEach() can lead to unanticipated behavior confusions, particularly with parallel streams.

  • Side effects: Use .forEach() to mutate only within the lambda expression. Let external collections be!
  • Concurrent collection: For mutating shared objects, consider thread-safe collections or synchronized blocks to prevent concurrency blues.

Multi-threading made easy with Stream API

The Stream API offers out-of-the-box multi-threading for your performance boost:

  • Parallel Streams: Use .parallelStream() for effortless operations parallelization.
  • Concurrent Collectors: Collectors.toConcurrentMap(), and the likes help with thread-safe concurrent collections.

Steer clear of stateful operations when dealing with parallel streams.

Readability and conciseness upgrades

Boost code readability with method references and static imports. It brings out the elegance within:

import static java.util.stream.Collectors.toList; // Look ma, no hands! Just the Integer::square charm. List<Integer> squares = nums.stream().map(Integer::square).collect(toList());

For cleansing nulls, trust filter(Objects::nonNull). Your list will be squeaky clean!

// No room for null here. Nope, nada! List<String> nonNullList = list.stream().filter(Objects::nonNull).collect(toList());

Performance optimization tricks

Dealing with performance-sensitive apps? Here's a route you can take:

  • Microbenchmarks: Tools like JMH can measure performance differences with precision.
  • Eclipse Collections: Explore APIs like collectIf() for potential speed enhancements.

Always evaluate performance impact specific to your operations.

Expert advice for streaming

Some wisdom bits for Stream API users:

  • Caution with parallel streams: Avoid unneeded complexity. It's not always about more performance juice.
  • Adopt the functional style: It's declarative, intuitive. Immutable transformations using .map() set the stage for this.
  • Beware of overhead: Streaming operations carry an overhead. Use wisely!