Definition
- Pipeline description: calling
stream().filter().map()builds a chain of operation descriptors like a recipe. No element is processed yet. - Pull-based: the terminal operation drives execution by requesting elements one at a time from upstream.
- One element in-flight: for stateless operations (filter, map), only one element is being processed at any instant, requiring O(1) memory.
- Loop fusion: filter, map, and other stateless operations execute on the same element before the next element is pulled.
- Short-circuit: findFirst and limit stop the pipeline as soon as the result is determined.
- Stateful exception: sorted and distinct must buffer elements, requiring O(n) memory.
A Collection is like the National Bank of India vault with 10,000 cheques stored inside. A Stream is like the cheque-clearing conveyor belt: it stores zero cheques. Cheques enter from the vault (source), pass through verification (filter), get stamped (map), and reach the destination (terminal). The belt pulls one cheque at a time. If the destination says "I only need 5 cleared cheques" (limit/findFirst), the belt stops. The remaining 9,995 cheques are never pulled from the vault.
Understanding the Concepts
Why do streams not store elements?
A Stream does not allocate memory to hold elements. It is a description of how to process elements from a source. When you call stream().filter().map(), Java builds a chain of operation descriptors. No element is processed until a terminal operation triggers the pipeline.
How does pull-based execution work?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
public class PullBasedDemo {
record Transaction(String id, BigDecimal amount, boolean settled) {}
public static void main(String[] args) {
List<Transaction> txns = List.of(
new Transaction("TXN-001", new BigDecimal("5000"), false),
new Transaction("TXN-002", new BigDecimal("45000"), true),
new Transaction("TXN-003", new BigDecimal("78000"), true)
);
// findFirst pulls only until it gets ONE result
Optional<BigDecimal> first = txns.stream()
.peek(t -> System.out.println("Source pulled: " + t.id()))
.filter(Transaction::settled)
.peek(t -> System.out.println("Filter passed: " + t.id()))
.map(Transaction::amount)
.findFirst();
System.out.println("Result: Rs." + first.orElse(BigDecimal.ZERO));
}
}
Source pulled: TXN-001
Source pulled: TXN-002
Filter passed: TXN-002
Result: Rs.45000
TXN-003 was never pulled from the source because findFirst stopped after receiving the first matching element.
What is loop fusion?
Multiple stateless operations execute in a single pass per element, not separate passes per operation.
package com.bank.core;
import java.util.List;
public class FusionDemo {
public static void main(String[] args) {
List.of("SAV2001", "SAV2002", "CUR3001").stream()
.peek(a -> System.out.println("1-filter: " + a))
.filter(a -> a.startsWith("SAV"))
.peek(a -> System.out.println("2-map: " + a))
.map(String::toLowerCase)
.forEach(a -> System.out.println("3-result: " + a));
}
}
1-filter: SAV2001
2-map: SAV2001
3-result: sav2001
1-filter: SAV2002
2-map: SAV2002
3-result: sav2002
1-filter: CUR3001
Each element goes through all stages before the next element is pulled. CUR3001 was dropped by filter, so map and result never see it.
What about stateful operations?
Stateless operations (filter, map, flatMap, peek) process one element at a time with O(1) memory. Stateful operations must buffer:
| Operation | Stateful? | Memory | Notes |
|-----------|-----------|--------|-------|
| filter | No | O(1) | Checks one element independently |
| map | No | O(1) | Transforms one element |
| sorted | Yes | O(n) | Buffers entire stream before sorting |
| distinct | Yes | O(n) | Maintains HashSet of seen elements |
| limit | Yes | O(1) | Counter, short-circuits |
How do infinite streams work?
package com.bank.core;
import java.util.List;
import java.util.stream.Stream;
public class InfiniteStreamDemo {
public static void main(String[] args) {
// Infinite source + short-circuit = finite result
List<String> ids = Stream.iterate(1, n -> n + 1)
.map(n -> "TXN-" + String.format("%06d", n))
.filter(id -> id.endsWith("5")) // every 10th
.limit(3) // stop after 3
.toList();
System.out.println(ids);
}
}
[TXN-000005, TXN-000015, TXN-000025]
The infinite source generates elements forever, but limit(3) stops the pipeline after three matches. Only 25 elements were ever generated.
What are the performance implications?
Filter before sorted reduces the number of elements that need buffering. Limit before sorted is pointless because sorted must see all elements anyway. Place stateless operations (filter, map) early in the pipeline and stateful operations (sorted, distinct) later.
What should a senior engineer know?
The Spliterator is the low-level engine that drives stream traversal. tryAdvance pulls one element, forEachRemaining processes all remaining, and trySplit divides for parallel processing. Understanding Spliterator helps when creating custom stream sources or debugging performance issues.
Visual Overview
Pull-based execution (findFirst):
──────────────────────────────────
findFirst: "give me ONE element"
↑
map: "give me ONE element" → transforms → passes up
↑
filter: "give me ONE element" → test → pass or drop
↑
source: yields TXN-001 → filter drops
source: yields TXN-002 → filter passes → map transforms
→ findFirst receives → STOPS!
TXN-003 NEVER pulled!
Loop Fusion (single pass per element):
──────────────────────────────────────
Element 1: filter → map → collect (one element through all stages)
Element 2: filter → map → collect
Element 3: filter → drop (never reaches map or collect)
NOT: filter-ALL → map-ALL → collect-ALL
Key Takeaway
A Stream does not store elements. It is a pipeline description that processes elements on demand from a source. Execution is pull-based: the terminal operation drives the pipeline by requesting elements one at a time. Each element flows through all stateless stages before the next element is pulled, requiring O(1) memory.
This design enables three powerful capabilities. Loop fusion executes filter, map, and collect in a single pass per element, not three separate loops. Short-circuit operations like findFirst and limit stop the pipeline as soon as the result is determined. Infinite streams work because pull-based laziness means only requested elements are generated.
The exception is stateful operations like sorted and distinct, which must buffer elements internally. Sorted needs all elements before it can sort, requiring O(n) memory. Understanding stateless versus stateful helps order operations optimally: filter before sorted reduces the buffer size.
Remember one sentence: streams store nothing and pull elements one at a time, so filter-map-collect is one pass, not three.
---