Course Content
Stream API
Stream API
Processing Elements with the forEach() Method
You're already familiar with the forEach()
method since you've used it in practice to print each element of a collection to the console. Let's take a closer look at it and explore its implementations.
It provides a convenient way to process data in a functional style. However, it's important to note that the processing order depends on the stream type and the method used.
There are two implementations of the forEach()
method in the Stream API. Let’s go over them one by one.
forEach Method
Executes an action on each element, but the processing order is not guaranteed in parallel streams.
The forEach()
method takes a Consumer<T>
as an argument—a functional interface that defines an operation for each element in the stream. It's commonly used for logging, printing, or performing side effects.
Practical Example
In an online store, users need to receive notifications about personalized discounts. Since the order doesn’t matter, forEach()
is used with a parallel stream for faster execution.
Main
package com.example; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> customers = List.of( "alice@example.com", "bob@example.com", "charlie@example.com" ); // Parallel stream for faster processing Stream<String> customerStream = customers.parallelStream(); customerStream.forEach(email -> sendDiscountEmail(email)); } private static void sendDiscountEmail(String email) { System.out.println("Sending discount email to: " + email); // Simulating email sending process } }
This code simulates sending personalized discount emails to customers. Since parallelStream()
is used, emails are sent in no particular order for improved speed. The forEach()
method applies the given action to each email.
forEachOrdered Method
Executes an action while preserving the order of elements, even in parallel streams.
Like forEach
, this method also accepts a Consumer<T>
, but it ensures that elements are processed in the same order as they appear in the original stream. This is useful when maintaining sequence is crucial.
Practical Example
Imagine a payment system where each transaction must be processed in the exact order it arrives. If payments are handled out of order, it could lead to errors, such as incorrect balance calculations.
Main
package com.example; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> payments = List.of( "Payment #5001 - $100", "Payment #5002 - $250", "Payment #5003 - $75" ); Stream<String> paymentStream = payments.parallelStream(); paymentStream.forEachOrdered(payment -> processPayment(payment)); } private static void processPayment(String payment) { System.out.println("Processing payment: " + payment); } }
This code simulates a payment processing system where transactions must be handled in the order they arrive. The use of parallelStream()
boosts performance, but forEachOrdered()
ensures the sequence remains intact.
Performance Comparison: forEach() vs. forEachOrdered()
Let's measure how much faster forEach()
executes compared to forEachOrdered()
when working with parallel streams. To do this, you'll create a list of 10 million elements, process them using both methods by computing the square root of each number, and record the execution time.
Main
package com.example; import java.util.List; import java.util.stream.IntStream; import java.util.ArrayList; public class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); IntStream.range(0, 10_000_000).forEach(numbers::add); // Create a list with 10 million elements // Measure execution time of `forEach()` long startTime = System.nanoTime(); numbers.parallelStream().forEach(num -> process(num)); long forEachTime = System.nanoTime() - startTime; // Measure execution time of `forEachOrdered()` startTime = System.nanoTime(); numbers.parallelStream().forEachOrdered(num -> process(num)); long forEachOrderedTime = System.nanoTime() - startTime; // Print results System.out.println("forEach execution time: " + forEachTime / 1_000_000 + " ms"); System.out.println("forEachOrdered execution time: " + forEachOrderedTime / 1_000_000 + " ms"); } private static void process(int num) { // Simulate workload Math.sqrt(num); } }
The forEach()
method processes elements without preserving order, allowing the stream to freely distribute tasks across available processor cores. This maximizes performance, as each thread can pick elements in any order and process them in parallel without restrictions.
The forEachOrdered()
method preserves the original order of elements, which requires additional synchronization. In a parallel stream, elements are first split into chunks for processing, but their results must then be reassembled in the correct order before being passed to the processing method.
1. Which functional interface does the forEach
method accept?
2. What output can this code produce?
Thanks for your feedback!