Stream basics: source → intermediate → terminal
Lazy pipelines, common operations, and the single-use rule.
Streams: declarative data processing
A stream is a lazy pipeline — you describe what to do, and nothing executes until a terminal operation is called. Every stream has three parts:
- Source — produces elements
- Intermediate operations — transform elements lazily (return a new stream)
- Terminal operation — triggers execution, produces a result or side-effect
List<String> names = List.of("Alice", "Bob", "Charlie", "Anna");
long count = names.stream() // source
.filter(s -> s.startsWith("A")) // intermediate — lazy
.map(String::toUpperCase) // intermediate — lazy
.count(); // terminal — executes the pipeline
// count = 2
Creating streams:
Stream<String> s1 = collection.stream();
Stream<String> s2 = Stream.of("a", "b", "c");
IntStream s3 = IntStream.range(0, 5); // 0, 1, 2, 3, 4
IntStream s4 = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
Key intermediate operations:
| Op | What it does |
|---|---|
| filter(Predicate) | keeps elements that pass |
| map(Function) | transforms each element |
| flatMap(Function) | maps each to a stream, then flattens |
| distinct() | removes duplicates (uses equals) |
| sorted() / sorted(Comparator) | sorts elements |
| limit(n) | keeps at most n elements |
| skip(n) | discards the first n elements |
| peek(Consumer) | side-effect for debugging without consuming the stream |
Laziness in practice: filter and map do not iterate the list; they register an operation to be applied when the terminal op fires. This enables short-circuiting: findFirst() stops after the first match, potentially reading only a few elements.