If Java interviews had a syllabus, the Stream API would be chapter one. "Given a list of employees, find the highest-paid in each department" — that single prompt, in dozens of variations, appears in machine rounds and face-to-face interviews for every level from fresher to lead. Streams are also how you will actually write data-handling code at work.
This guide assumes you know lambda expressions and the core functional interfaces. If Predicate and Function are fuzzy, read those first — every stream method signature is built from them.
What a stream is (and is not)
A stream is a pipeline of operations over a sequence of elements. Three properties define it:
- It does not store data. It pulls from a source — usually a collection from the Collections Framework, an array, or a generator.
- It never modifies the source. Operations produce new streams and new results.
- It is lazy and single-use. Nothing runs until the terminal operation, and after that the stream is dead.
The mental model: source → zero or more intermediate operations → one terminal operation.
List<String> result = names.stream() // source
.filter(n -> n.length() > 3) // intermediate
.map(String::toUpperCase) // intermediate
.sorted() // intermediate
.toList(); // terminal (Java 16+; use
// collect(Collectors.toList()) on 8)
The core trio: filter, map, collect
Ninety percent of real pipelines are combinations of these three. Here is a complete, runnable example you can extend for practice:
import java.util.List;
import java.util.stream.Collectors;
public class CoreTrio {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Asha", "Backend", 950_000),
new Employee("Ravi", "Backend", 700_000),
new Employee("Meena", "QA", 600_000),
new Employee("Kiran", "Frontend", 820_000)
);
// Names of employees earning above 8L, alphabetically
List<String> high = staff.stream()
.filter(e -> e.salary() > 800_000) // Predicate
.map(Employee::name) // Function
.sorted()
.collect(Collectors.toList());
System.out.println(high); // [Asha, Kiran]
// Total payroll of the Backend team
double backendCost = staff.stream()
.filter(e -> e.dept().equals("Backend"))
.mapToDouble(Employee::salary) // primitive stream
.sum();
System.out.println(backendCost); // 1650000.0
}
}
Two things to notice. filter takes a Predicate and keeps elements where it returns true; map takes a Function and transforms each element. And mapToDouble switches to a primitive DoubleStream, which gives you sum(), average() and max() without boxing — reach for mapToInt/mapToLong/mapToDouble whenever you do arithmetic.
Terminal operations beyond collect
| Operation | Returns | Use for |
|---|---|---|
collect(collector) |
List, Set, Map, String... | Building containers |
forEach(consumer) |
void | Side effects (printing, saving) |
reduce(identity, op) |
T | Folding to a single value |
count() |
long | Counting matches |
anyMatch / allMatch / noneMatch |
boolean | Existence checks |
findFirst / findAny |
Optional<T> |
Grabbing one element |
min / max (comparator) |
Optional<T> |
Extremes |
Note how many return Optional — a stream may be empty, and the API refuses to hand you null. That is why Java Optional is the natural next topic after streams.
reduce deserves one example because interviewers use it to test whether you understand accumulation:
int totalChars = List.of("java", "stream", "api").stream()
.mapToInt(String::length)
.reduce(0, Integer::sum); // 0 + 4 + 6 + 3 = 13
The identity (0) is the starting value; the accumulator combines the running result with each element.
Common mistake: A common mistake beginners make is writing a pipeline with only intermediate operations —
list.stream().filter(...).map(...);— and wondering why nothing happened. Intermediate operations are lazy; without a terminal operation the pipeline never executes. If a stream "does nothing", check for the missing terminal call first.
Grouping and partitioning: the interview favourites
Collectors.groupingBy is the single most-asked stream construct in Indian product and service company interviews. It converts a stream into a Map<K, List<T>> — or, with a downstream collector, into counts, sums or nested maps.
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class GroupingDemo {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Asha", "Backend", 950_000),
new Employee("Ravi", "Backend", 700_000),
new Employee("Meena", "QA", 600_000),
new Employee("Kiran", "Frontend", 820_000)
);
// 1. Employees per department
Map<String, Long> headcount = staff.stream()
.collect(Collectors.groupingBy(
Employee::dept, Collectors.counting()));
System.out.println(headcount);
// {QA=1, Frontend=1, Backend=2}
// 2. Highest paid per department — THE classic question
Map<String, Optional<Employee>> topEarner = staff.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.maxBy(
java.util.Comparator.comparingDouble(Employee::salary))));
topEarner.forEach((d, e) ->
System.out.println(d + " -> " + e.map(Employee::name).orElse("-")));
// 3. Partition: exactly two buckets, above/below 8L
Map<Boolean, List<String>> bands = staff.stream()
.collect(Collectors.partitioningBy(
e -> e.salary() > 800_000,
Collectors.mapping(Employee::name, Collectors.toList())));
System.out.println(bands);
// {false=[Ravi, Meena], true=[Asha, Kiran]}
}
}
partitioningBy is just groupingBy with a Predicate and exactly two keys (true/false). The downstream collector — counting(), maxBy(), mapping(), summingDouble() — is where the real expressive power lives; practise swapping them.
Interview note: When you answer "highest salary per department", say out loud that
maxByreturns anOptionalbecause a group could theoretically be empty, and showCollectors.collectingAndThen(maxBy(...), Optional::get)as the unwrapping trick. Mentioning the Optional before the interviewer asks is a strong senior signal.
Laziness and short-circuiting
Streams process elements vertically (one element through the whole pipeline), not horizontally (each operation over all elements). Combined with short-circuiting operations, this can skip most of the work:
Optional<String> first = names.stream()
.filter(n -> n.startsWith("A")) // runs only until a match is found
.findFirst();
With findFirst, the pipeline stops at the first element that survives the filter — if it is the 3rd of 10 million elements, the other 9,999,997 are never touched. limit, anyMatch, allMatch and noneMatch short-circuit the same way. This is why streams can even process infinite sources: Stream.iterate(1, n -> n * 2).limit(10) yields the first ten powers of two and stops.
Parallel streams: powerful, frequently misused
Swap .stream() for .parallelStream() and the pipeline runs on the common ForkJoinPool across CPU cores. Whether that helps depends on three factors:
- Data size — thousands of elements at minimum; splitting has overhead.
- Work per element — CPU-heavy transforms benefit; trivial ones drown in coordination cost.
- Source splittability —
ArrayListand arrays split cleanly;LinkedListanditeratesources split poorly.
Rules that keep you safe: never mutate shared state inside a parallel pipeline (collect with Collectors instead), avoid it entirely for IO-bound work, and benchmark sequential vs parallel before shipping. A parallel stream that shares the common pool with your web server's other work can also starve unrelated tasks.
Pro tip: Default to sequential streams. In years of code reviews, genuinely beneficial
parallelStream()calls are rare, while accidental shared-state bugs from them are not. Parallelism is an optimisation you apply after measuring, never a starting point.
Common mistakes checklist
- Reusing a consumed stream →
IllegalStateException. Create a new one. - Mutating the source or external variables inside
map/filter— keep lambdas pure; collect results instead. - Using
mapwhereflatMapis needed — if your function returns a List or Stream, you wantflatMap. - Forcing everything into one pipeline — a 12-operation stream is worse than two short ones with a named intermediate list.
collect(Collectors.toList())inside a loop that could be onegroupingBy.
Where streams fit in your preparation
Streams are the layer where all your Java 8 knowledge becomes visible in interviews — lambdas supply the syntax, functional interfaces supply the types, and collectors supply the answers. In the Java Full Stack course we run a dedicated stream-question week: 40 graded pipeline exercises from filter/map basics up to nested groupingBy, because fluency here is the difference between describing streams and solving with them under interview pressure.
Frequently Asked Questions
What is the difference between intermediate and terminal operations?
Do streams modify the original collection?
Can I reuse a Java stream after a terminal operation?
What is the difference between map and flatMap?
When should I use parallel streams?
Is a stream better than a for loop?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

