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.
Sir, the question was one sentence. Why is my code six lines of bookkeeping?
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.
Does it make three passes over the list?
No, and that's the clever part. Let's watch the orders flow.
What is the Stream API?
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.
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
list.stream(), Arrays.stream(arr).Loop vs stream
| for loop | Stream pipeline | |
|---|---|---|
| Style | How: counters, ifs, temp variables | What: filter, map, collect |
| Modifies source? | Can | Never |
| Reusable? | Run the loop again | Make a new stream; one use each |
| Early exit | break | findFirst, anyMatch, limit |
| Parallel | Write thread code yourself | parallelStream() (with care) |
| Best for | Complex control flow, early returns, index math | Filtering, 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.”
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.
Predicatemap
Turn each element into another value (or type) with a Function. Same length.
Functionreduce
Combine all elements into one value: sum, max, product. Needs an identity (0 for sums).
BinaryOperatorcollect
Build a List, Set, Map or String with Collectors: toList, groupingBy, joining.
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” 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().
Building and running a pipeline
Get a stream from a source
A Stream object linked to the list. No elements are read yet.orders.stream()Add intermediate operations
Each call returns a new Stream stage that remembers its lambda and the previous stage. Still nothing runs..filter(a -> a >= 999) .map(a -> a * 118 / 100)Call a terminal operation
Now the source is asked for elements, one at a time..reduce(0, Integer::sum);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 likesortedare the exception: they must collect all elements first.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.
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.
orders.stream()makes a stream: a pipeline that will pull the orders through, one by one.filterkeeps only the elements for which the Predicate is true: 450 and 80 are dropped.mapturns each element into a new value with a Function: amount → amount with GST.reduce(0, Integer::sum)combines all values into one, starting from 0.- The pipeline gives ₹5,662: total billing of the big orders including GST.
- Streams don't modify their source. You describe what you want; the stream does the looping.
filterandmapare intermediate operations: calling them only records the steps. Nothing is printed, nothing is computed.reduceis a terminal operation: it starts the flow. The first element, 450, is tested by filter and dropped.- The next element goes through all stages before any other element starts: filter, then map, then into the sum.
- Elements move one at a time, “vertically” through the pipeline, not stage by stage over the whole list.
- No intermediate lists are built, so streams can process huge data with little memory, and stop early when possible (
findFirst,limit). - After the terminal operation the stream is used up. Calling another operation on
sthrowsIllegalStateException.
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.
- Today's orders, each with a city and an amount.
Collectors.groupingBy(Order::city, …)puts each order into a bucket for its city. (TreeMap keeps the cities sorted.)- groupingBy takes a second collector that decides what to do with each bucket.
summingInt(Order::amount)adds up the amounts inside each bucket.- The result is a
Map<String, Integer>ready for the report: one pipeline instead of a loop, a map and manual updates.
1. Dashboards and reports
Sales per city, top products, orders per hour: groupingBy and summing collectors build the numbers behind the charts.
2. API responses
Services map database entities to response objects: products.stream().map(ProductDto::from).toList().
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.
4. Validation
allMatch, anyMatch and noneMatch answer “are all items in stock?” or “is any field empty?” in one line.
Simplified: very large datasets are usually aggregated in the database (SQL GROUP BY) or a data platform; streams shine for data already in memory.
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);
}
}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.
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.
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.
+25000, -1200, -450, +3000, -8500, -90
2 big debits, total Rs 9700
Approach
filter(t -> t < -500)keeps big debits.mapToInt(Math::abs)turns them into positive ints, giving anIntStream.- Use two pipelines: one to
count, one tosum.
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.
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.
6 products with price and stock
Cheapest in stock: Stand (Rs 249), Cover (Rs 299), Card reader (Rs 349)
Approach
filteron stock > 0.sorted(Comparator.comparingInt(...))by price, thenlimit(3).mapto a label andcollect(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.
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.
marks 92, 67, 45, 81, 38, 74, 59
{A=2, B=2, C=2, F=1}Approach
- A small method
grade(int)returns the letter. groupingBy(Main::grade, TreeMap::new, counting())gives a sorted map grade → count.- 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.
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.
Even numbers squared
From 1–10, keep even numbers, square them and collect into a list.
1..10
[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());
}
}How it works: IntStream.rangeClosed makes numbers without a list. boxed() turns ints into Integers so they can go into a List.
Names in upper case
Convert a list of names to upper case with map and print the new list.
asha, ravi, meena
[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 + ")");
}
}How it works: map creates new values; the original list is unchanged. toList() (Java 16+) returns an unmodifiable list.
Sum and average
Find the sum and average of daily sales using an IntStream.
1200, 950, 1800, 600
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);
}
}How it works: primitive streams have sum, average, min, max. average returns OptionalDouble because an empty stream has no average (Lesson 14.4).
Distinct sorted cities
From a list of delivery cities with repeats, print the unique cities in alphabetical order, comma-separated.
Pune, Goa, Pune, Delhi, Goa, Agra
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(", ")));
}
}How it works: distinct removes duplicates using equals; sorted uses natural order; joining builds one String.
Find the first match
Find the first order above ₹2,000 and check whether any order is below ₹100.
450, 1200, 2600, 80, 3100
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);
}
}How it works: findFirst and anyMatch are short-circuiting: they stop as soon as they know the answer, without looking at the rest.
Word frequency
Count how often each word appears in a review, case-insensitively, in alphabetical order.
"Good food good price GOOD service"
{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);
}
}How it works: Arrays.stream streams an array. groupingBy with counting is the standard word-count pattern used in text analytics.
Revenue per category
Sum revenue per product category from a list of sales, sorted by category name.
6 sales in 3 categories
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));
}
}How it works: this is SQL's GROUP BY category SUM(amount), done in memory.
Top 2 customers by spend
From orders (customer, amount), total each customer's spend and print the top 2 spenders.
7 orders, 4 customers
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()));
}
}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.
Pending deliveries report
From delivery records, list the ids of PENDING deliveries older than 2 days, sorted, and print how many there are.
6 deliveries
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);
}
}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.
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.
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
sortedmust 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 5with an index.
Avoid: switching to parallelStream() before fixing the order.
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
toMapthrows 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 withgroupingBy(Customer::phone). - Log or report duplicates so data quality can be fixed.
Avoid: catching the exception and returning an empty map.
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”.