Definition
- Source provides elements: Collection, array, file, generator, or range.
- Intermediate operations are lazy transformations that return a new Stream:
filter,map,sorted,distinct,flatMap,limit,skip,peek. - Terminal operations trigger execution and produce a result:
collect,forEach,reduce,count,findFirst,anyMatch. - Lazy evaluation means intermediate operations do nothing until a terminal triggers them.
- Single-use means after a terminal, the stream is consumed and throws IllegalStateException on reuse.
- No storage means the stream does not hold elements in memory; it processes them on demand from the source.
A Collection is like the bank vault at National Bank of India: it stores cash physically. You go in, count it, sort it, take some out. A Stream is like the cash-counting machine on the assembly line: cash flows through it, gets counted, sorted, and filtered, but the machine itself stores nothing. The cash comes from the vault (source), passes through machines (intermediate operations), and ends at the bag (terminal operation). The machines sit idle until the manager presses start (terminal).
Understanding the Concepts
What is the Stream API?
The Stream API (java.util.stream) was introduced in Java 8 for declarative data processing. Instead of writing imperative for-loops with explicit accumulators, you describe what to compute and the framework handles execution.
What are the three components?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
public class StreamComponentsDemo {
record Transaction(String accountId, BigDecimal amount, boolean settled) {}
public static void main(String[] args) {
List<Transaction> transactions = List.of(
new Transaction("SAV2001", new BigDecimal("45000"), true),
new Transaction("SAV2002", new BigDecimal("12000"), false),
new Transaction("SAV2001", new BigDecimal("78000"), true),
new Transaction("CUR3001", new BigDecimal("250000"), true)
);
BigDecimal total = transactions.stream() // SOURCE
.filter(Transaction::settled) // INTERMEDIATE (lazy)
.map(Transaction::amount) // INTERMEDIATE (lazy)
.reduce(BigDecimal.ZERO, BigDecimal::add); // TERMINAL (triggers all)
System.out.println("Settled total: Rs." + total);
}
}
Settled total: Rs.373000
How is a Stream different from a Collection?
| Aspect | Collection | Stream |
|--------|-----------|--------|
| Storage | Stores elements in memory | Stores nothing: processes on the fly |
| Evaluation | Eager: all elements exist upfront | Lazy: computed only when terminal runs |
| Reuse | Reusable: iterate many times | Single-use: consumed after terminal |
| Mutability | Can add, remove, modify | Cannot modify source |
| How you work | Imperative: for-loop, if, accumulator | Declarative: filter, map, collect |
| Parallelism | Manual thread management | Built-in: parallelStream() |
What are the stream sources?
package com.bank.core;
import java.util.*;
import java.util.stream.*;
import java.math.BigDecimal;
public class StreamSources {
public static void main(String[] args) {
// From Collection:
List.of("SAV2001", "SAV2002").stream();
// From array:
Stream.of("RELIANCE", "TCS", "HDFC");
Arrays.stream(new int[]{100, 200, 300});
// From generator (infinite!):
Stream.generate(() -> BigDecimal.valueOf(Math.random()));
Stream.iterate(BigDecimal.ONE, amt -> amt.multiply(BigDecimal.TWO));
// Bounded iterate (Java 9):
Stream.iterate(1, n -> n <= 100, n -> n + 1);
// Empty:
Stream.empty();
}
}
What are the intermediate operations?
All intermediate operations are lazy and return a new Stream.
filter(Predicate): keep matching elementsmap(Function): transform each elementflatMap(Function): transform and flatten (one-to-many)sorted()/sorted(Comparator): sort (stateful, buffers all)distinct(): remove duplicates (stateful, uses HashSet)limit(n): take first n (short-circuit)skip(n): drop first npeek(Consumer): debug observation (do not use for business logic)
What are the terminal operations?
Terminal operations trigger execution and consume the stream.
collect(Collector): accumulate into collection, string, or maptoList(): collect to unmodifiable list (Java 16)forEach(Consumer): execute action on eachreduce(identity, BinaryOperator): fold into single valuecount(): count elementsfindFirst()/findAny(): return Optional (short-circuit)anyMatch/allMatch/noneMatch: boolean (short-circuit)min(Comparator)/max(Comparator): return Optional
What happens if you reuse a stream?
package com.bank.core;
import java.util.stream.Stream;
public class StreamReuseDemo {
public static void main(String[] args) {
Stream<String> stream = Stream.of("SAV2001", "SAV2002", "CUR3001");
long count = stream.count(); // OK: terminal #1
System.out.println("Count: " + count);
try {
stream.toList(); // THROWS IllegalStateException!
} catch (IllegalStateException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Count: 3
Error: stream has already been operated upon or closed
What are common mistakes?
package com.bank.core;
import java.util.List;
public class StreamMistakes {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "SAV2002");
// WRONG: no terminal operation, pipeline does nothing
accounts.stream()
.filter(a -> a.startsWith("SAV"))
.peek(System.out::println); // Nothing printed! No terminal!
// CORRECT: add a terminal operation
accounts.stream()
.filter(a -> a.startsWith("SAV"))
.forEach(System.out::println); // Prints both accounts
}
}
SAV2001
SAV2002
What should a senior engineer know?
Streams are designed to complement Collections, not replace them. Collections store data (the data layer). Streams process data (the computation layer). The typical pattern is: collection.stream() followed by pipeline operations followed by collect() back to a Collection. Streams are particularly powerful for read-only analytical queries, aggregation, grouping, and parallel processing of large datasets.
Visual Overview
Stream Pipeline:
─────────────────────────────────────────────
transactions.stream() ← SOURCE
│
▼
.filter(Transaction::settled) ← INTERMEDIATE (lazy)
│
▼
.map(Transaction::amount) ← INTERMEDIATE (lazy)
│
▼
.reduce(ZERO, BigDecimal::add) ← TERMINAL (triggers all)
│
▼
Rs.373000 ← RESULT
Without terminal: NOTHING executes.
After terminal: stream is CONSUMED.
Collection vs Stream: ┌───────────────────┐ ┌───────────────────┐ │ Collection (Vault) │ │ Stream (Machine) │ │ Stores elements │ │ Processes elements │ │ Eager │ │ Lazy │ │ Reusable │ │ Single-use │ │ Mutable │ │ Immutable pipeline │ │ size(), get() │ │ filter(), map() │ └───────────────────┘ └───────────────────┘
Key Takeaway
The Stream API is a pipeline-based abstraction with three components: source, intermediate operations, and terminal operation. The source provides elements from a Collection, array, file, or generator. Intermediate operations like filter, map, sorted, and distinct are lazy: they build a pipeline description but execute nothing until a terminal operation triggers them. Terminal operations like collect, reduce, forEach, and count trigger the entire pipeline and produce a result.
Streams differ from Collections in five fundamental ways. Streams do not store elements. Streams are lazy while collections are eager. Streams are single-use while collections are reusable. Streams are declarative while collection processing is imperative. Streams support built-in parallelism via parallelStream().
In Rajesh's settlement report service at National Bank of India, the stream pipeline filters settled transactions, maps to amounts, and reduces to a total. Without the terminal reduce, nothing executes. After the terminal, the stream is consumed and cannot be reused.
Remember one sentence: a stream pipeline is source to lazy intermediates to terminal, and nothing runs until the terminal triggers it.
---