Stream API: filter, map, reduce

Most business code is “take a collection, keep some items, transform them, and sum or group them”. Loops can do it, but the intent gets lost in counters and temporary lists. The Stream API lets you write that as a pipeline: filter, map, reduce, collect. You'll learn the operations you'll use daily, why streams are lazy, and how to build reports with groupingBy.

Module 14 · Modern Java⏱ 25 minIntermediate9 practice problems
1 The story

The report hidden inside a loop

The canteen owner asks Riya: “What did big orders, ₹999 and above, bring in today, including GST?” Her answer is a loop with an if, a calculation, a running total and a temporary list. It works, but a week later even she has to read it twice.

int total = 0; for (int a : orders) { if (a >= 999) { total += a * 118 / 100; } } filter map reduce keep big → add GST → add up myphatshala.in
Riya

Sir, the question was one sentence. Why is my code six lines of bookkeeping?

Arjun Sir

Because a loop says how to do it. A stream says what you want: keep orders ≥ 999, add GST, sum them. Three steps, three words: filter, map, reduce.

Riya

Does it make three passes over the list?

Arjun Sir

No, and that's the clever part. Let's watch the orders flow.

2 Definition

What is the Stream API?

Formal definition (write this in exams)

A stream (java.util.stream.Stream<T>) is a sequence of elements from a source (a collection, array, I/O channel or generator) that supports aggregate operations in a pipeline. A pipeline consists of a source, zero or more intermediate operations (such as filter, map, sorted), which are lazy and return a new stream, and one terminal operation (such as collect, reduce, forEach, count), which triggers processing and produces a result or side effect. Streams do not store elements, do not modify their source, and can be consumed only once. Primitive streams (IntStream, LongStream, DoubleStream) avoid boxing.

In simple words

A stream is a conveyor belt with stations. You set up the stations (filter, map…), and nothing moves until someone at the end asks for the result.

Words you'll meet in this lesson

SourceWhere elements come from: list.stream(), Arrays.stream(arr).
Intermediate opReturns a new Stream; lazy: filter, map, sorted, distinct, limit.
Terminal opProduces a result and starts the work: collect, reduce, count.
LazyNothing happens until it's needed.
CollectorA recipe for building a result: a list, a map, a String.
Short-circuitingStops early: findFirst, anyMatch, limit.

Loop vs stream

for loopStream pipeline
StyleHow: counters, ifs, temp variablesWhat: filter, map, collect
Modifies source?CanNever
Reusable?Run the loop againMake a new stream; one use each
Early exitbreakfindFirst, anyMatch, limit
ParallelWrite thread code yourselfparallelStream() (with care)
Best forComplex control flow, early returns, index mathFiltering, transforming, aggregating

Exam tip: “Intermediate operations are lazy and return a Stream; a terminal operation triggers execution and returns a non-stream result. A stream cannot be reused after a terminal operation (IllegalStateException), and it does not modify its source.”

3 Types and ways to use it

The operations you'll use

// create                     intermediate (lazy, return a Stream)      terminal (start the work)
list.stream()                  filter(Predicate)                         forEach(Consumer)
Arrays.stream(array)           map(Function)                             collect(Collectors.toList()) / toList()
Stream.of(a, b, c)             mapToInt / mapToDouble                    reduce(identity, BinaryOperator)
IntStream.range(0, n)          sorted() / sorted(Comparator)             count() / sum() / average()
Files.lines(path)              distinct() / limit(n) / skip(n)           findFirst() / anyMatch / allMatch
                               peek(Consumer)  (debugging)               min / max (Comparator)

filter

Keep elements that match a Predicate. The stream may get shorter.

Predicate

map

Turn each element into another value (or type) with a Function. Same length.

Function

reduce

Combine all elements into one value: sum, max, product. Needs an identity (0 for sums).

BinaryOperator

collect

Build a List, Set, Map or String with Collectors: toList, groupingBy, joining.

Collector

See the laziness

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> orders = List.of(450, 1200, 80, 2600);
        Stream<Integer> pipeline = orders.stream()
                .filter(a -> { System.out.println("  filter " + a); return a >= 999; })
                .map(a -> { System.out.println("  map " + a); return a * 118 / 100; });
        System.out.println("pipeline built, nothing ran yet");
        int total = pipeline.reduce(0, Integer::sum);
        System.out.println("total = " + total);
    }
}
pipeline built, nothing ran yet filter 450 filter 1200 map 1200 filter 80 filter 2600 map 2600 total = 4484

“pipeline built” is printed first: building the pipeline ran nothing. Then each order goes through filter and map before the next order starts.

Keep lambdas in a stream free of side effects. Don't add to an outside list or change shared variables inside map or filter; collect the result instead. Side effects make streams hard to read and break completely with parallelStream().

4 Step by step

Building and running a pipeline

  1. Get a stream from a source

    orders.stream()
    A Stream object linked to the list. No elements are read yet.
  2. Add intermediate operations

        .filter(a -> a >= 999)
        .map(a -> a * 118 / 100)
    Each call returns a new Stream stage that remembers its lambda and the previous stage. Still nothing runs.
  3. Call a terminal operation

        .reduce(0, Integer::sum);
    Now the source is asked for elements, one at a time.
  4. Each element flows through every stage

    450: filter says no, so the next element starts. 1200: filter yes, map gives 1416, reduce adds it. Stateful steps like sorted are the exception: they must collect all elements first.
  5. The result, and the end of the stream

    The terminal operation returns its result. The stream is now consumed; build a new one for another query.
5 See it happen

filter, map, reduce: both views

Left: orders moving through the stations. Right: what really happens in time: nothing until the terminal operation, then one order at a time through all stations.

filter, map, reduce: both views 6 steps
What you see orders.stream().filter(a -> a >= 999) .map(a -> a * 118 / 100).reduce(0, Integer::sum) source filter ≥ 999 map +GST reduce sum 450 1200 80 2600 999 1200 2600 999 450 ✗ 80 ✗ 1416 3068 1178 0+1416 +3068 +1178 5662 the list itself is unchanged: [450, 1200, 80, 2600, 999] one value comes out of the pipeline Five orders go in. filter keeps orders of ₹999+. map adds 18% GST to each. reduce adds them up. Result: ₹5,662. The source list is untouched. myphatshala.in
Behind the screen Stream s = orders.stream() .filter(...).map(...); // intermediate int total = s.reduce(0, Integer::sum); // terminal console (what each stage prints) (nothing yet: no terminal operation) filter 450 ✗ filter 450 ✗ filter 1200 → map 1200 → +1416 filter 450 ✗ filter 1200 → map 1200 → +1416 filter 80 ✗ filter 450 ✗ filter 1200 → map 1200 → +1416 filter 80 ✗ filter 2600 → map 2600 → +3068 filter 999 → map 999 → +1178 filter 450 ✗ filter 1200 → map 1200 → +1416 filter 80 ✗ filter 2600 → map 2600 → +3068 filter 999 → map 999 → +1178 total 5662 Building the pipeline runs nothing. reduce starts the flow: 450 fails. 1200 goes all the way through. 80 fails the filter. 2600 and 999 go through. One pass over the data. myphatshala.in
6 Real-world use

Streams in real systems

Wherever Java code produces reports, dashboards or API responses from collections, you'll find stream pipelines.

Watch it with an example: a city-wise sales report in one pipeline

A restaurant chain wants total sales per city from today's orders. groupingBy collects the stream into a map.

Group orders by city and sum 5 steps
Animated example Pune 450 Mumbai 900 Pune 120 Nagpur 300 Mumbai 640 Mumbai [900, 640] Rs 1540 Nagpur [300] Rs 300 Pune [450, 120] Rs 570 groupingBy(Order::city, TreeMap::new, summingInt(Order::amount)) {Mumbai=1540, Nagpur=300, Pune=570} Five orders from three cities. groupingBy sorts them into buckets. One collector per bucket… …sums each bucket. A Map: city → total. myphatshala.in

1. Dashboards and reports

Sales per city, top products, orders per hour: groupingBy and summing collectors build the numbers behind the charts.

orders→groupingBy(city, summingInt)→bar chart
[{…},{…}]

2. API responses

Services map database entities to response objects: products.stream().map(ProductDto::from).toList().

entities→map(Dto::from)→JSON list

3. File and log processing

Files.lines(path) is a lazy stream of lines: filter errors, map to fields, count, without loading the whole file.

app.log→lines().filter(...).count()→error rate

4. Validation

allMatch, anyMatch and noneMatch answer “are all items in stock?” or “is any field empty?” in one line.

cart→allMatch(i -> i.inStock())→allow checkout

Simplified: very large datasets are usually aggregated in the database (SQL GROUP BY) or a data platform; streams shine for data already in memory.

7 Run the code

A canteen report in three pipelines

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> orders = List.of("dosa:80", "thali:120", "chai:15", "dosa:80", "biryani:220", "chai:15");

        int revenue = orders.stream()
                .mapToInt(o -> Integer.parseInt(o.split(":")[1]))
                .sum();

        List<String> premium = orders.stream()
                .filter(o -> Integer.parseInt(o.split(":")[1]) >= 100)
                .map(o -> o.split(":")[0])
                .distinct()
                .sorted()
                .toList();

        Map<String, Long> counts = orders.stream()
                .map(o -> o.split(":")[0])
                .collect(Collectors.groupingBy(d -> d, TreeMap::new, Collectors.counting()));

        System.out.println("revenue: Rs " + revenue);
        System.out.println("premium dishes: " + premium);
        System.out.println("orders per dish: " + counts);
    }
}
revenue: Rs 530 premium dishes: [biryani, thali] orders per dish: {biryani=1, chai=2, dosa=2, thali=1}
  • mapToInt(...).sum()Turn each order into its price (an int) and add them.
  • filter → map → distinct → sorted → toList()Names of dishes costing ₹100+, without repeats, A–Z.
  • groupingBy(d -> d, TreeMap::new, counting())How many times each dish was ordered, sorted by name.
  • three orders.stream() callsEach query needs its own stream; the list can be streamed any number of times.
8 Line by line

A pipeline, one element at a time

Recorded from the real JVM. Line 6 is one statement, but watch the lambda frames: filter 450, filter 1200, map 1200, filter 80… Each order goes through before the next starts.

Worked examples: real problems, solved and traced in memory

Three report-style problems, each solved with one stream pipeline instead of loops and temporary lists. Watch the lambda frames: each element passes through all stages before the next.

Example 1Banking · statements

Total debits above ₹500 in a statement

Problem statement

From a list of transactions (positive = credit, negative = debit), find how many debits were larger than ₹500 and their total. Use streams.

Input
+25000, -1200, -450, +3000, -8500, -90
Expected output
2 big debits, total Rs 9700

Approach

  1. filter(t -> t < -500) keeps big debits.
  2. mapToInt(Math::abs) turns them into positive ints, giving an IntStream.
  3. Use two pipelines: one to count, one to sum.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2E-commerce · catalogue

Top 3 cheapest in-stock products

Problem statement

Show the three cheapest products that are in stock, as "name (Rs price)" strings, joined by commas.

Input
6 products with price and stock
Expected output
Cheapest in stock: Stand (Rs 249), Cover (Rs 299), Card reader (Rs 349)

Approach

  1. filter on stock > 0.
  2. sorted(Comparator.comparingInt(...)) by price, then limit(3).
  3. map to a label and collect(Collectors.joining(", ")).

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Education · results

Grade counts with groupingBy

Problem statement

Given students' marks, count how many students got each grade: A (80+), B (60–79), C (40–59), F (below 40). Print the grades in order.

Input
marks 92, 67, 45, 81, 38, 74, 59
Expected output
{A=2, B=2, C=2, F=1}

Approach

  1. A small method grade(int) returns the letter.
  2. groupingBy(Main::grade, TreeMap::new, counting()) gives a sorted map grade → count.
  3. Print the map.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 14.3 in one screen

  • Pipeline = source → intermediate ops (lazy) → one terminal op.
  • filter (Predicate), map (Function), sorted, distinct, limit, skip are intermediate.
  • collect/toList, reduce, count, sum, forEach, findFirst, anyMatch are terminal.
  • Nothing runs until the terminal op; then elements flow one at a time through all stages.
  • Streams never modify the source and can be used once.
  • IntStream (mapToInt, range) for numbers: sum, average, max.
  • Collectors: toList, joining, groupingBy + counting/summingInt.

Interview and viva questions

What is the difference between intermediate and terminal operations?

Intermediate operations (filter, map, sorted…) return a new stream and are lazy; they only describe the pipeline. Terminal operations (collect, reduce, forEach, count…) produce a result or side effect and trigger the processing. A pipeline has exactly one terminal operation.

What does “streams are lazy” mean, and why is it useful?

Elements are processed only when a terminal operation needs them, one at a time through the whole pipeline. This avoids intermediate collections, allows short-circuiting (findFirst stops at the first match) and even works on infinite streams with limit.

map vs flatMap?

map turns each element into exactly one new element. flatMap turns each element into a stream of elements and flattens them into one stream, e.g. orders → their line items: orders.stream().flatMap(o -> o.items().stream()).

Collection vs Stream?

A collection stores elements in memory and can be iterated many times; a stream stores nothing, computes elements on demand from a source, and can be consumed once. Collections are about data; streams are about computation on data.

When should you not use parallelStream()?

For small collections, I/O-bound work, operations with side effects or shared mutable state, or when order matters and you'd need costly ordering. Parallel streams use the common ForkJoinPool; measure before using them.

Practice problems

Replace loops with pipelines: filter, map, sorted, limit, reduce and collect. All outputs are fixed. The last three are business tasks.

P1Easy

Even numbers squared

From 1–10, keep even numbers, square them and collect into a list.

Input
1..10
Expected output
[4, 16, 36, 64, 100]
Show solution and explanation
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        System.out.println(IntStream.rangeClosed(1, 10)
                .filter(n -> n % 2 == 0)
                .map(n -> n * n)
                .boxed()
                .toList());
    }
}
[4, 16, 36, 64, 100]

How it works: IntStream.rangeClosed makes numbers without a list. boxed() turns ints into Integers so they can go into a List.

P2Easy

Names in upper case

Convert a list of names to upper case with map and print the new list.

Input
asha, ravi, meena
Expected output
[ASHA, RAVI, MEENA] (original: [asha, ravi, meena])
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("asha", "ravi", "meena");
        List<String> upper = names.stream().map(String::toUpperCase).toList();
        System.out.println(upper + " (original: " + names + ")");
    }
}
[ASHA, RAVI, MEENA] (original: [asha, ravi, meena])

How it works: map creates new values; the original list is unchanged. toList() (Java 16+) returns an unmodifiable list.

P3Easy

Sum and average

Find the sum and average of daily sales using an IntStream.

Input
1200, 950, 1800, 600
Expected output
sum 4550, average 1137.5
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> sales = List.of(1200, 950, 1800, 600);
        int sum = sales.stream().mapToInt(Integer::intValue).sum();
        double avg = sales.stream().mapToInt(Integer::intValue).average().orElse(0);
        System.out.println("sum " + sum + ", average " + avg);
    }
}
sum 4550, average 1137.5

How it works: primitive streams have sum, average, min, max. average returns OptionalDouble because an empty stream has no average (Lesson 14.4).

P4Medium

Distinct sorted cities

From a list of delivery cities with repeats, print the unique cities in alphabetical order, comma-separated.

Input
Pune, Goa, Pune, Delhi, Goa, Agra
Expected output
Agra, Delhi, Goa, Pune
Show solution and explanation
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> cities = List.of("Pune", "Goa", "Pune", "Delhi", "Goa", "Agra");
        System.out.println(cities.stream().distinct().sorted().collect(Collectors.joining(", ")));
    }
}
Agra, Delhi, Goa, Pune

How it works: distinct removes duplicates using equals; sorted uses natural order; joining builds one String.

P5Medium

Find the first match

Find the first order above ₹2,000 and check whether any order is below ₹100.

Input
450, 1200, 2600, 80, 3100
Expected output
first big order: 2600, any tiny order: true
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> orders = List.of(450, 1200, 2600, 80, 3100);
        int first = orders.stream().filter(o -> o > 2000).findFirst().orElse(-1);
        boolean tiny = orders.stream().anyMatch(o -> o < 100);
        System.out.println("first big order: " + first + ", any tiny order: " + tiny);
    }
}
first big order: 2600, any tiny order: true

How it works: findFirst and anyMatch are short-circuiting: they stop as soon as they know the answer, without looking at the rest.

P6Medium

Word frequency

Count how often each word appears in a review, case-insensitively, in alphabetical order.

Input
"Good food good price GOOD service"
Expected output
{food=1, good=3, price=1, service=1}
Show solution and explanation
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        String review = "Good food good price GOOD service";
        Map<String, Long> freq = Arrays.stream(review.toLowerCase().split(" "))
                .collect(Collectors.groupingBy(w -> w, TreeMap::new, Collectors.counting()));
        System.out.println(freq);
    }
}
{food=1, good=3, price=1, service=1}

How it works: Arrays.stream streams an array. groupingBy with counting is the standard word-count pattern used in text analytics.

P7Business problem

Revenue per category

Sum revenue per product category from a list of sales, sorted by category name.

Input
6 sales in 3 categories
Expected output
Fashion: Rs 2498
Grocery: Rs 680
Mobile: Rs 13498
Show solution and explanation
import java.util.*;
import java.util.stream.*;

class Sale {
    String category;
    int amount;
    Sale(String c, int a) { category = c; amount = a; }
}

public class Main {
    public static void main(String[] args) {
        List<Sale> sales = List.of(new Sale("Grocery", 450), new Sale("Mobile", 12999), new Sale("Grocery", 230),
                                   new Sale("Fashion", 1599), new Sale("Mobile", 499), new Sale("Fashion", 899));
        Map<String, Integer> byCat = sales.stream()
                .collect(Collectors.groupingBy(s -> s.category, TreeMap::new, Collectors.summingInt(s -> s.amount)));
        byCat.forEach((cat, total) -> System.out.println(cat + ": Rs " + total));
    }
}
Fashion: Rs 2498 Grocery: Rs 680 Mobile: Rs 13498

How it works: this is SQL's GROUP BY category SUM(amount), done in memory.

P8Business problem

Top 2 customers by spend

From orders (customer, amount), total each customer's spend and print the top 2 spenders.

Input
7 orders, 4 customers
Expected output
Ravi Rs 2200
Meena Rs 2100
Show solution and explanation
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        String[][] orders = {{"Asha", "1200"}, {"Ravi", "300"}, {"Asha", "450"}, {"Meena", "2100"},
                             {"Imran", "800"}, {"Ravi", "1900"}, {"Imran", "150"}};
        Map<String, Integer> spend = Arrays.stream(orders)
                .collect(Collectors.groupingBy(o -> o[0], Collectors.summingInt(o -> Integer.parseInt(o[1]))));
        spend.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
                .limit(2)
                .forEach(e -> System.out.println(e.getKey() + " Rs " + e.getValue()));
    }
}
Ravi Rs 2200 Meena Rs 2100

How it works: two pipelines: one to total per customer, one over the map's entries to rank them. Ties would need a second comparator for a stable order.

P9Business problem

Pending deliveries report

From delivery records, list the ids of PENDING deliveries older than 2 days, sorted, and print how many there are.

Input
6 deliveries
Expected output
3 late deliveries: [D4, D5, D7]
Show solution and explanation
import java.util.*;
import java.util.stream.*;

class Delivery {
    String id, status;
    int ageDays;
    Delivery(String i, String s, int a) { id = i; status = s; ageDays = a; }
}

public class Main {
    public static void main(String[] args) {
        List<Delivery> all = List.of(new Delivery("D7", "PENDING", 3), new Delivery("D2", "DONE", 5),
                new Delivery("D9", "PENDING", 1), new Delivery("D4", "PENDING", 4),
                new Delivery("D1", "DONE", 1), new Delivery("D5", "PENDING", 6));
        List<String> late = all.stream()
                .filter(d -> d.status.equals("PENDING"))
                .filter(d -> d.ageDays > 2)
                .map(d -> d.id)
                .sorted()
                .toList();
        System.out.println(late.size() + " late deliveries: " + late);
    }
}
3 late deliveries: [D4, D5, D7]

How it works: chaining two filters reads like the business rule. The pipeline is easy to extend: add .limit(10) for a dashboard widget.

Real-world scenario questions (asked in company interviews)

Stream questions in interviews are usually about correctness and performance traps, not syntax.

Code review · side effectsFresher

Adding to a list inside forEach

A teammate writes List<String> out = new ArrayList<>(); names.stream().filter(...).forEach(n -> out.add(n.trim()));. It works. What would you suggest?

The interviewer is checking: idiomatic, side-effect-free streams.

Show a strong answer
  • Use the stream's result instead of mutating an outside list: List<String> out = names.stream().filter(...).map(String::trim).toList();
  • The side-effect version breaks (lost elements, exceptions) if someone later switches to parallelStream(), because ArrayList isn't thread-safe.

Avoid: “forEach is the stream version of a for loop”; collect is.

Performance · orderJunior

Sorting 10 lakh items to show 5

A product page runs products.stream().sorted(byPrice).filter(p -> p.inStock()).limit(5) on 10 lakh products. It's slow. How would you reorder or rewrite it?

The interviewer is checking: operation order and stateful operations.

Show a strong answer
  • sorted must see every element before passing any on, so it sorts all 10 lakh, including out-of-stock ones.
  • Filter first: filter(inStock).sorted(byPrice).limit(5) sorts fewer items.
  • Better still, let the database do WHERE in_stock ORDER BY price LIMIT 5 with an index.

Avoid: switching to parallelStream() before fixing the order.

Bug · duplicate keysJunior

IllegalStateException: Duplicate key

Building a map of customers by phone with Collectors.toMap(Customer::phone, c -> c) crashes in production with “Duplicate key 98xxxxxx10”. Explain and fix.

The interviewer is checking: collector behaviour with real data.

Show a strong answer
  • toMap throws when two elements produce the same key; two customers share a phone number (family accounts, data errors).
  • Decide the business rule: keep the latest with a merge function toMap(k, v, (a, b) -> b), or keep all with groupingBy(Customer::phone).
  • Log or report duplicates so data quality can be fixed.

Avoid: catching the exception and returning an empty map.

Performance · parallelMid

parallelStream made the API slower

To speed up a product-details API, a developer changed ids.stream().map(this::fetchFromDb) to parallelStream(). Under load the whole server got slower. Why?

The interviewer is checking: parallel streams and the common pool.

Show a strong answer
  • Parallel streams run on the shared ForkJoinPool (about one thread per core). Blocking DB calls tie up those few threads, and every request in the JVM competes for them.
  • For I/O, fetch in one batched query (WHERE id IN (...)), or use a dedicated executor / virtual threads with CompletableFuture.
  • Keep parallel streams for CPU-heavy work on large in-memory data, and measure.

Avoid: “parallel is always faster”.