Streams API in Java
Introduction
Prior to Java 8, processing collections usually involved writing boilerplate for loops, managing iterators, and mutating state. The introduction of the Streams API (java.util.stream) shifted Java towards a declarative, functional style of programming.
Instead of defining how to iterate over data, you declare what you want to achieve.
1. What is a Stream?
A Stream is not a data structure. It does not store elements. Rather, it is a pipeline of computational operations applied to a source of data (like a Collection, an array, or an I/O channel).
Key characteristics of Streams:
- Declarative: You specify the desired result, not the step-by-step control flow.
- Lazy: Computation is deferred until the very end.
- Consumable: A stream can only be traversed once.
- Parallelizable: Streams can be easily processed across multiple threads without writing explicit multithreading code.
2. Creating Streams
There are multiple ways to generate a stream in Java.
From Collections
The most common way is calling .stream() on a Collection.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
From Arrays
Use Arrays.stream().
int[] numbers = {1, 2, 3, 4};
IntStream stream = Arrays.stream(numbers);
Using Stream.of()
Stream<String> stream = Stream.of("Java", "Python", "C++");
Infinite Streams
You can generate infinite streams using Stream.generate() or Stream.iterate().
// Infinite stream of random numbers
Stream<Double> randoms = Stream.generate(Math::random);
// Infinite stream: 0, 2, 4, 6...
Stream<Integer> evens = Stream.iterate(0, n -> n + 2);
3. The Stream Pipeline Architecture
A Stream pipeline consists of three phases:
- Source: Where the data comes from.
- Intermediate Operations: Transformations applied to the stream (e.g.,
filter,map). These return a new Stream. - Terminal Operation: Produces a result (e.g.,
collect,count) or a side effect (e.g.,forEach).
Example Pipeline
import java.util.Arrays;
import java.util.List;
public class StreamPipeline {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sumOfSquaresOfEvens = numbers.stream() // 1. Source
.filter(n -> n % 2 == 0) // 2. Intermediate
.map(n -> n * n) // 2. Intermediate
.reduce(0, Integer::sum); // 3. Terminal
System.out.println(sumOfSquaresOfEvens); // (4 + 16 + 36) = 56
}
}
4. Parallel Streams
One of the greatest benefits of the Streams API is trivial parallelization. By changing .stream() to .parallelStream(), Java uses the Fork/Join pool to process elements concurrently.
List<String> bigList = getVeryLargeList();
// Processes concurrently
long count = bigList.parallelStream()
.filter(s -> s.startsWith("A"))
.count();
Note: Parallel streams should be used cautiously. For small datasets or I/O-bound operations, the overhead of managing threads can make parallel streams slower than sequential ones.
Summary
The Streams API represents a paradigm shift in Java, promoting functional programming concepts. It allows developers to write clean, highly readable, and easily parallelizable code for data processing.