Skip to main content

8.4 Stream API

← Back to Chapter 8: Java 8 Features

The Stream API (java.util.stream) represents a powerful, declarative mechanism for processing sequences of elements. Instead of writing explicit loops with conditional logic, you define high-level transformation pipelines.

STREAM API
|
┌───────┴───────┐
↓ ↓
SOURCE PIPELINE
|
┌──────┴──────┐
↓ ↓
INTERMEDIATE TERMINAL
| |
filter/map/... collect/toList
|
LAZY

What is Streams?​

Stream API is a way to process data from a collection in a clean, readable and functional style.

Suppose you have:

List<Integer> numbers = [10, 15, 20, 25, 30]

You want:

Find all numbers greater than 20 and convert them into their squares.

Without Stream:

for each number
if number > 20
calculate square
add to another list

With Stream:

numbers.stream()
.filter(n -> n > 20)
.map(n -> n * n)
.toList();

Result:

[625, 900]

A Stream itself does not store data. It takes data from a source such as a collection, array, generator, etc., and passes it through operations.

Why was Stream API introduced?​

To avaoid multiple loops

Before Java 8, processing collections usually required a lot of loops.

For example:

List<String> result = new ArrayList<>();

for (String name : names) {
if (name.startsWith("A")) {
result.add(name.toUpperCase());
}
}

Java 8 allows:

List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.toList();

The second version is easier to read because it describes what you want, rather than every step of how to do it.

Streams vs Collections​

┌───────────────────────────────────────┐
│ Collection: In-memory Data Storage │ -> Emphasizes how data is stored
└───────────────────────────────────────┘
│
▼ (.stream())
┌───────────────────────────────────────┐
│ Stream: Declarative Pipeline & Compute│ -> Emphasizes how data is transformed
└───────────────────────────────────────┘
AspectCollectionStream
PurposeStores and holds data in memoryComputes, transforms, and transfers data
StorageElements exist physically in memoryDoes not store elements
MutationModifies underlying elements (add, remove)Pure & functional: Does not mutate source
IterationExternal iteration (for-each, Iterator)Internal iteration (handled by runtime)
EvaluationEager (constructed completely upfront)Lazy (intermediate ops only run when terminal op fires)
ReusabilityTraversed multiple timesSingle-use only (throws IllegalStateException if reused)

Stream Pipeline​

A Stream pipeline consists of three core phases:

[ Source ] ──► [ Intermediate Operations (Lazy) ] ──► [ Terminal Operation (Eager) ]
filter() ──► map() ──► sorted() collect() / forEach()

Intermediate Operations (Lazy)​

Intermediate operations transform a stream into another stream and are not executed until a terminal operation is invoked.

MethodDescription
filter(Predicate<T>)Retains elements matching the condition
map(Function<T, R>)Transforms each element into another object
flatMap(Function<T, Stream<R>>)Flattens nested stream structures (e.g., List<List<T>> into Stream<T>)
distinct()Filters out duplicates using equals()
sorted() / sorted(Comparator)Sorts elements naturally or by custom comparator
limit(long n)Truncates the stream to length at most n
skip(long n)Discards the first n elements
peek(Consumer<T>)Inspects elements for debugging without modifying them

map() vs flatMap():​

// map() transforms 1 element to 1 element
List<String> words = List.of("hello", "world");
List<Integer> lengths = words.stream().map(String::length).toList();

// flatMap() flattens 1-to-many structures (1 element to Stream)
List<List<String>> nested = List.of(List.of("A", "B"), List.of("C", "D"));
List<String> flattened = nested.stream()
.flatMap(Collection::stream)
.toList(); // ["A", "B", "C", "D"]

Terminal Operations (Eager)​

Terminal operations produce a non-stream result (value, collection) or side-effect and consume the stream.

MethodFunctionality
Accumulators
collect(Collector)Collects elements into a container (like a List or Set)
reduce(BinaryOperator)Combines elements into a single summary value
count()Counts the number of elements in the stream
min()Finds the minimum element based on a comparator
max()Finds the maximum element based on a comparator
Matchers
anyMatch(Predicate)Checks if at least one element matches the condition
allMatch(Predicate)Checks if all elements match the condition
noneMatch(Predicate)Checks if no elements match the condition
Searchers
findFirst()Returns an Optional<T> containing the first element
findAny()Returns an Optional<T> containing any element
Iterators
forEach(Consumer)Performs an action for each element
forEachOrdered(Consumer)Performs an action for each element in stream order

Version wise Updates​

Java VersionStream Change
Java 8Stream API introduced
Java 9takeWhile()
Java 9dropWhile()
Java 9ofNullable()
Java 9Enhanced iterate()
Java 10No major Stream API addition
Java 11No major Stream API addition
Java 12Collectors.teeing()
Java 16Stream.toList()
Java 16mapMulti()
Java 17+No fundamental Stream redesign
Current Java 26Core API remains based on the same Stream model, with later additions available

Parallel Streams (Multithreading)​

Parallel streams automatically split the workload across CPU cores using the ForkJoinPool.commonPool():

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

// Executes operations concurrently across threads
numbers.parallelStream()
.map(n -> expensiveCalculation(n))
.forEach(result -> System.out.println(Thread.currentThread().getName() + " -> " + result));

Performance Caveat: Parallel streams introduce thread management overhead and synchronisation costs. Use them only for CPU-heavy tasks or massive datasets where elements are stateless and independent.