Stream API
Reading Time: 12 Minutes
Difficulty: Intermediate
Topic Summaryโ
The Stream API lets you process collections of data in a clean, pipeline-style way. Instead of writing loops and if-statements manually, you describe what you want โ filter, transform, sort, collect โ and Java does the how. Think of it like an assembly line for your data.
What You'll Learnโ
- What a Stream is and how it differs from a Collection
- How to create streams from different sources
- Intermediate operations:
filter(),map(),sorted(),distinct(),limit() - Terminal operations:
collect(),forEach(),count(),reduce(),findFirst() - Real-world example: filtering students with marks > 80
Prerequisitesโ
- Lambda Expressions (Lesson 1)
- Functional Interfaces (Lesson 2)
- Basic Java Collections (List, Set)
Explanationโ
What is a Stream?โ
A Stream is a sequence of elements that you can process one by one, using a pipeline of operations. Streams:
- Do not store data (unlike a List or Array)
- Do not modify the original data source
- Are lazy โ intermediate operations don't run until a terminal operation is called
- Can only be consumed once โ after a terminal operation, the stream is closed
Stream vs Collectionโ
| Feature | Collection | Stream |
|---|---|---|
| Stores data? | โ Yes | โ No |
| Modifies source? | โ Can | โ Never |
| Iteration | External (you write the loop) | Internal (stream handles it) |
| Reusable? | โ Yes | โ No (one-time use) |
| Lazy evaluation? | โ No | โ Yes |
Creating Streamsโ
From a List:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();
From an Array:
int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers);
Using Stream.of():
Stream<String> stream = Stream.of("Java", "Python", "Go");
Infinite stream (with limit):
Stream<Integer> naturals = Stream.iterate(1, n -> n + 1);
Intermediate Operations (Lazy โ return a new Stream)โ
Intermediate operations transform the stream and return another stream. They don't do anything until a terminal operation is called.
filter(Predicate) โ Keep elements that matchโ
list.stream()
.filter(n -> n > 10) // keep only numbers > 10
map(Function) โ Transform each elementโ
list.stream()
.map(s -> s.toUpperCase()) // convert each to uppercase
sorted() โ Sort elements (natural order or custom)โ
list.stream()
.sorted() // alphabetical / natural
.sorted((a, b) -> b - a) // custom: descending
distinct() โ Remove duplicatesโ
Stream.of(1, 2, 2, 3, 3, 3).distinct() // โ 1, 2, 3
limit(n) โ Take only first N elementsโ
stream.limit(5) // take only 5 elements
skip(n) โ Skip first N elementsโ
stream.skip(2) // skip first 2, take rest
peek() โ Debug without consuming (look but don't change)โ
stream.peek(e -> System.out.println("Seeing: " + e))
Terminal Operations (Eager โ consume the Stream)โ
Terminal operations trigger the pipeline to run and produce a result.
collect(Collectors.toList()) โ Gather results into a Listโ
List<String> result = stream.collect(Collectors.toList());
forEach(Consumer) โ Do something with each elementโ
stream.forEach(s -> System.out.println(s));
count() โ Count elementsโ
long count = stream.count();
reduce(identity, BinaryOperator) โ Combine all elements into oneโ
int sum = Stream.of(1,2,3,4,5).reduce(0, (a, b) -> a + b);
// sum = 15
findFirst() โ Get the first element (returns Optional)โ
Optional<String> first = stream.findFirst();
anyMatch(), allMatch(), noneMatch() โ Test conditionsโ
boolean anyOver80 = marks.stream().anyMatch(m -> m > 80);
boolean allPassed = marks.stream().allMatch(m -> m >= 40);
min() and max() โ Find smallest or largestโ
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
The Stream Pipelineโ
A typical stream pipeline looks like:
source โ intermediate ops โ intermediate ops โ terminal op
list.stream() // source
.filter(...) // intermediate
.map(...) // intermediate
.sorted() // intermediate
.collect(...) // terminal โ kicks everything off
Real-World Analogyโ
Think of a Stream pipeline like a water treatment plant:
- Water enters from a source (river/lake = your list)
- It passes through filters (remove dirt =
filter()) - It gets treated/transformed (purified =
map()) - It gets sorted into bottles (sorted =
sorted()) - Finally it's collected in tanks (
collect())
Each stage works on the water flowing through โ it doesn't store it.
Code Exampleโ
Example 1: Filter Students with Marks > 80โ
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("Alice", "Bob", "Charlie", "Diana", "Eve");
List<Integer> marks = Arrays.asList(92, 45, 83, 67, 95);
// Find students with marks > 80
System.out.println("=== Students with marks > 80 ===");
for (int i = 0; i < students.size(); i++) {
int mark = marks.get(i);
String name = students.get(i);
if (mark > 80) {
System.out.println(name + ": " + mark);
}
}
}
}
Outputโ
=== Students with marks > 80 ===
Alice: 92
Charlie: 83
Eve: 95
Example 2: Full Stream Pipelineโ
import java.util.*;
import java.util.stream.*;
public class FullStreamPipeline {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 9, 2, 7, 4, 6, 8, 3);
// Pipeline: filter evens, square them, sort descending, take top 3
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // keep even: 8, 2, 4, 6, 8
.map(n -> n * n) // square: 64, 4, 16, 36, 64
.sorted((a, b) -> b - a) // sort descending: 64, 64, 36, 16, 4
.distinct() // remove duplicates: 64, 36, 16, 4
.limit(3) // take first 3: 64, 36, 16
.collect(Collectors.toList()); // collect to list
System.out.println("Top 3 unique squared even numbers: " + result);
// Count of odd numbers
long oddCount = numbers.stream()
.filter(n -> n % 2 != 0)
.count();
System.out.println("Count of odd numbers: " + oddCount);
// Sum all numbers using reduce
int total = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum of all numbers: " + total);
// Find any number greater than 7
Optional<Integer> bigNumber = numbers.stream()
.filter(n -> n > 7)
.findFirst();
bigNumber.ifPresent(n -> System.out.println("First number > 7: " + n));
}
}
Outputโ
Top 3 unique squared even numbers: [64, 36, 16]
Count of odd numbers: 5
Sum of all numbers: 56
First number > 7: 8
Example 3: Working with Stringsโ
import java.util.*;
import java.util.stream.*;
public class StringStreamDemo {
public static void main(String[] args) {
List<String> cities = Arrays.asList(
"Mumbai", "Delhi", "Bangalore", "Chennai", "Mumbai", "Pune", "Delhi"
);
// Unique cities starting with 'M', uppercased, sorted
List<String> result = cities.stream()
.distinct() // remove duplicates
.filter(c -> c.startsWith("M")) // starts with M
.map(String::toUpperCase) // uppercase
.sorted() // alphabetical
.collect(Collectors.toList());
System.out.println("Result: " + result);
// Join all unique cities with comma
String joined = cities.stream()
.distinct()
.sorted()
.collect(Collectors.joining(", "));
System.out.println("All cities: " + joined);
}
}
Outputโ
Result: [MUMBAI]
All cities: Bangalore, Chennai, Delhi, Mumbai, Pune
Common Mistakesโ
- โ Mistake: Reusing a stream after a terminal operation โ โ Fix: Streams can only be consumed once. Create a new stream each time
- โ Mistake: Expecting intermediate operations to run without a terminal operation โ โ
Fix: Streams are lazy โ nothing runs until a terminal op (
collect,forEach,countetc.) is called - โ Mistake: Modifying the source list inside a stream lambda โ โ
Fix: Never modify the source collection inside stream operations โ it causes
ConcurrentModificationException - โ Mistake: Using
stream()onnullโ โ Fix: Always check for null before calling.stream(), or useOptional - โ Mistake: Forgetting that
findFirst()returnsOptional<T>, notTโ โ Fix: Use.get(),.orElse(), or.ifPresent()to access the value
Best Practicesโ
- Prefer streams over manual
forloops for data processing โ they're cleaner and more readable - Keep stream pipelines readable โ one operation per line
- Use
parallelStream()for large datasets (but measure performance before using it) - Avoid side effects inside
map()โ keep transformations pure - Use
Collectors.joining(),Collectors.groupingBy(),Collectors.toMap()for powerful aggregations
Interview Questionsโ
Q: What is the difference between map() and filter() in streams?
A: filter() takes a Predicate and keeps only elements that match โ it doesn't change elements, just removes some. map() takes a Function and transforms each element into something new โ every element stays but is converted.
Q: What is the difference between intermediate and terminal operations?
A: Intermediate operations (like filter, map, sorted) return a new Stream and are lazy โ they don't execute until a terminal operation triggers the pipeline. Terminal operations (like collect, forEach, count) produce a result and consume the stream.
Q: Can you reuse a Stream?
A: No. Once a terminal operation is called on a stream, the stream is closed and cannot be reused. You need to create a new stream from the source each time.
Q: What is the difference between findFirst() and findAny()?
A: findFirst() always returns the first element in encounter order. findAny() may return any element โ it's designed for parallel streams where order doesn't matter and performance matters more.
Q: What is reduce() in streams?
A: reduce() combines all stream elements into a single result using a BinaryOperator. For example, reduce(0, Integer::sum) adds all numbers together. It takes an identity value (starting point) and an accumulator function.
Quick Revisionโ
โ Stream = pipeline for processing data, not a data store
โ Intermediate ops are lazy (filter, map, sorted, distinct, limit)
โ Terminal ops trigger execution (collect, forEach, count, reduce, findFirst)
โ Streams are single-use โ create a new one each time
โ Use Collectors.toList() to collect results back into a list
Related Topicsโ
- Functional Interfaces
- Lambda Expressions
- Optional Class
- Method References
- Collectors API
Next Lessonโ
Lesson 4 โ Optional Class