By WebNest Studio Engineering Team
Java - Core Tutorial
Stream API
The Stream API, added in Java 8, provides a declarative way to process sequences of data — collections, arrays, or I/O sources — using a pipeline of operations rather than explicit loops. A stream describes what should happen to the data, not the mechanics of how to iterate over it.
Streams are built around a simple pipeline shape: a source produces elements, zero or more intermediate operations transform or filter them, and exactly one terminal operation triggers execution and produces a result or side effect.
Why it matters
Filtering a list, converting each element, grouping and totalling are the most common things done to data, and the Stream API expresses all of them as a short pipeline. Stream code is in every modern Java codebase, and stream questions are in almost every Java interview from junior level upwards.
The Stream Pipeline
A typical pipeline looks like collection.stream().filter(...).map(...).sorted(...).collect(...). filter keeps elements matching a Predicate, map transforms each element with a Function, and sorted orders elements — these are all intermediate operations that return a new stream. The pipeline only actually runs when a terminal operation like collect, forEach, reduce, count, or anyMatch is called.
Lazy Evaluation
Intermediate operations are lazy: calling .filter() or .map() does not process any elements immediately. Java only walks through the source data once, when a terminal operation is invoked, and it processes each element through the entire chain of intermediate operations before moving to the next one. This means a stream with no terminal operation does nothing at all, and short-circuiting operations like findFirst() can stop processing early.
Streams Are Not Reusable
Once a terminal operation consumes a stream, that stream is considered closed and cannot be reused — calling another operation on it throws an IllegalStateException. If you need to process the same data twice, you must create a new stream from the source collection each time.
Example
A filter-map-sorted-collect pipeline plus reduce
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "amy", "Bob", "diana");
List<String> result = names.stream()
.filter(n -> n.length() > 3) // "amy" and "Bob" are dropped here
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
System.out.println(result);
int totalLength = names.stream()
.mapToInt(String::length)
.reduce(0, Integer::sum);
System.out.println("Total length: " + totalLength);
}
}
[CHARLIE, DIANA]
Total length: 18
Common Mistakes
- Trying to reuse the same stream for two terminal operations, which throws IllegalStateException because streams are single-use.
- Calling only intermediate operations (filter, map) without a terminal operation and expecting something to happen — nothing executes until a terminal operation runs.
- Using streams for simple, short loops where a plain for-loop would be clearer and just as efficient — streams shine with multi-step transformations, not every iteration.
- Forgetting that stream operations should generally be free of side effects, since the order of processing is not always guaranteed, especially with parallel streams.
Exercise
From the list of numbers, keep those greater than 6, double each one, sort the result and collect it into a list. Then print the sum of that list.
Expected output: [16, 24, 40] then 80
import java.util.List;
import java.util.stream.Collectors;
public class StreamPipeline {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 12, 8, 3, 20);
// TODO: filter (> 6), map (double), sorted, collect to a list
// TODO: print the list
// TODO: print the sum of the list
}
}
Show hints
- Chain
filter,mapandsorted, and finish withcollect(Collectors.toList()). - For the sum,
mapToInt(Integer::intValue).sum()works on a stream of the result.
Show solution
import java.util.List;
import java.util.stream.Collectors;
public class StreamPipeline {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 12, 8, 3, 20);
List<Integer> result = numbers.stream()
.filter(n -> n > 6)
.map(n -> n * 2)
.sorted()
.collect(Collectors.toList());
System.out.println(result); // [16, 24, 40]
int sum = result.stream().mapToInt(Integer::intValue).sum();
System.out.println(sum); // 80
}
}
Quick quiz
1. A pipeline has filter and map but no terminal operation. What runs?
- A. Both operations
- B. Only filter
- C. Nothing
- D. It does not compile
Show answer
C. Intermediate operations are lazy. They run only when a terminal operation asks for results.
2. What happens if you call a terminal operation twice on the same stream?
- A. The pipeline runs again
- B. IllegalStateException is thrown
- C. The second call returns an empty result
- D. It does not compile
Show answer
B. A stream can be used once. Create a new stream from the source to run the pipeline again.
3. Does list.stream().filter(...) remove elements from list?
- A. Yes
- B. Only for an ArrayList
- C. No; a stream does not change its source
- D. Only if collect is called
Show answer
C. The pipeline produces a new result and leaves the original collection as it was.
Interview questions
What is the difference between intermediate and terminal operations?
Show answer
Intermediate operations, such as filter, map and sorted, return another stream and are lazy: they only describe what should happen. A terminal operation, such as collect, forEach, count or reduce, produces a result or a side effect and causes the whole pipeline to run. A pipeline has any number of intermediate operations and exactly one terminal operation.
What is the difference between map and flatMap?
Show answer
map turns each element into exactly one new element. flatMap turns each element into a stream of elements and joins all those streams into one. Mapping a list of orders to their item lists gives a stream of lists; flatMap gives a single stream of items.
How is a Stream different from a Collection?
Show answer
A collection stores elements in memory and can be read many times. A stream stores nothing: it carries elements from a source through a pipeline of operations, computes them on demand, can be used only once, and does not modify the source.
Key Points to Remember
- A stream pipeline is: source -> intermediate operations (filter, map, sorted) -> one terminal operation (collect, forEach, reduce).
- Intermediate operations are lazy; nothing executes until a terminal operation is invoked.
- A stream can be consumed by a terminal operation only once — it is not reusable.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering