JavaJava 8intermediate
Updated:

Java Streams: filter, map, collect and Beyond

5 min read

From your first filter-map-collect pipeline to groupingBy, reduce and parallel streams — a practical, interview-focused guide to the Java Stream API.

TL;DR – Quick Answer

A Java Stream is a pipeline that processes a sequence of elements declaratively: you chain intermediate operations like filter, map and sorted, then trigger execution with a terminal operation like collect, forEach or reduce. Streams do not store data or modify their source — they describe a computation that runs lazily, once, when the terminal operation is called.

On This Page

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 maxBy returns an Optional because a group could theoretically be empty, and show Collectors.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 splittabilityArrayList and arrays split cleanly; LinkedList and iterate sources 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 map where flatMap is needed — if your function returns a List or Stream, you want flatMap.
  • 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 one groupingBy.

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?
Intermediate operations (filter, map, sorted, distinct, limit) return another stream and are lazy — they do nothing until a terminal operation runs. Terminal operations (collect, forEach, reduce, count, findFirst, anyMatch) produce a result or side effect and consume the stream. Every pipeline needs exactly one terminal operation.
Do streams modify the original collection?
No. A stream never changes its source; operations like filter and map produce new streams and collect produces a new result container. If the original list appears modified, something in your lambda is mutating the elements themselves, which is a side effect you should avoid in stream code.
Can I reuse a Java stream after a terminal operation?
No. A stream is consumed by its terminal operation, and calling anything on it afterwards throws IllegalStateException with the message 'stream has already been operated upon or closed'. If you need to traverse the data again, create a new stream from the source collection.
What is the difference between map and flatMap?
map transforms each element one-to-one, so a Stream<List<String>> mapped stays a stream of lists. flatMap transforms each element into a stream and flattens all of them into a single stream, turning Stream<List<String>> into Stream<String>. Use flatMap whenever your mapping function returns a collection or stream.
When should I use parallel streams?
Only when you have a large number of elements, CPU-bound per-element work, and a source that splits well, like an ArrayList or an array. For small lists, IO-bound work, or ordered operations, parallel streams are often slower than sequential ones because of splitting and merging overhead. Always measure before and after.
Is a stream better than a for loop?
Neither is universally better. Streams excel at multi-step transformations — filter, map, group, collect — where they read like a description of the result. A plain loop is better for simple iteration, early exits with complex conditions, or when you need index access. Readability for the next developer is the deciding factor.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us