Chapter 21: Stream API (Java 8+)
The Stream API lets you process collections (lists, sets, arrays, etc.) in a declarative style — you tell what you want to do, not how to do it step-by-step. It’s like giving instructions to a smart assistant instead of doing everything manually.
Imagine we’re sitting together in a quiet Bandra café — it’s evening, the sea is calm outside, and I’m going to explain Streams like I’m teaching my younger brother who just discovered how much easier life becomes after learning them.
We’ll go super slowly, with real-life analogies, complete runnable programs, step-by-step breakdowns, tables, common mistakes with fixes, and tons of examples you can copy-paste and run right now.
Let’s dive in!
1. What is a Stream? (The Big Idea)
A Stream is not a data structure — it’s a pipeline for processing a sequence of elements (from a Collection, array, or generated data) in a functional style.
Key Characteristics:
- Not storing data — just processing data from a source
- Lazy evaluation — intermediate operations are not executed until a terminal operation is called
- Once consumed — a stream can be used only once (like a river: once water flows, you can’t use it again)
- Functional & immutable — operations return new streams (original data unchanged)
- Parallelizable — easy to make multi-threaded
Real-life analogy: Think of a factory assembly line:
- Source = raw materials (Collection)
- Intermediate operations = machines that transform (filter, map, sort…)
- Terminal operation = final product (collect to list, count, print…)
2. Creating Streams (Different Ways)
| Source | How to Create Stream | Example |
|---|---|---|
| Collection | collection.stream() | list.stream() |
| Array | Arrays.stream(array) or Stream.of(array) | Arrays.stream(numbers) |
| Static factory methods | Stream.of(…), Stream.iterate(), Stream.generate() | Stream.of(1,2,3) |
| Files / Lines | Files.lines(path) | Files.lines(Paths.get(“file.txt”)) |
| Infinite streams | Stream.iterate(seed, function) | Stream.iterate(0, n -> n + 2) (even numbers) |
Example 1: Different Ways to Create Streams
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import java.util.*; import java.util.stream.*; public class StreamCreationDemo { public static void main(String[] args) { // 1. From Collection List<String> cities = Arrays.asList("Mumbai", "Delhi", "Pune", "Bangalore"); Stream<String> cityStream = cities.stream(); // 2. From Array String[] fruits = {"Apple", "Banana", "Cherry"}; Stream<String> fruitStream = Arrays.stream(fruits); // 3. Stream.of() Stream<Integer> numbers = Stream.of(10, 20, 30, 40); // 4. Infinite stream (limited by limit()) Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2) .limit(10); // 0,2,4,...,18 // Print one of them evenNumbers.forEach(n -> System.out.print(n + " ")); } } |
Output:
|
0 1 2 3 4 5 6 |
0 2 4 6 8 10 12 14 16 18 |
3. Intermediate & Terminal Operations
Streams have two kinds of operations:
| Type | Description | Returns | Lazy? | Examples |
|---|---|---|---|---|
| Intermediate | Transform the stream, return new stream | Stream | Yes | filter(), map(), sorted(), distinct(), limit(), skip() |
| Terminal | Produce a result or side-effect, close the stream | Non-Stream (List, int, void…) | No | forEach(), collect(), reduce(), count(), anyMatch(), findFirst() |
Important: Intermediate operations are lazy — nothing happens until a terminal operation is called!
Example 2: Full Stream Pipeline
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.util.*; import java.util.stream.*; public class StreamPipelineDemo { public static void main(String[] args) { List<String> names = Arrays.asList( "Webliance", "Amit", "Priya", "Rahul", "Sneha", "Vikram", "Ananya" ); // Stream pipeline List<String> result = names.stream() // create stream .filter(name -> name.length() > 5) // intermediate: keep long names .map(String::toUpperCase) // intermediate: uppercase .sorted() // intermediate: sort alphabetically .limit(3) // intermediate: take first 3 .collect(Collectors.toList()); // terminal: collect to List System.out.println("Result: " + result); } } |
Output:
|
0 1 2 3 4 5 6 |
Result: [ANANYA, PRIYA, WEBLIANCE] |
Step-by-step:
- filter() → only names > 5 chars: Webliance, Priya, Rahul, Sneha, Vikram, Ananya
- map() → uppercase: WEBLIANCE, PRIYA, RAHUL, SNEHA, VIKRAM, ANANYA
- sorted() → ANANYA, PRIYA, RAHUL, SNEHA, VIKRAM, WEBLIANCE
- limit(3) → ANANYA, PRIYA, WEBLIANCE
- collect() → terminal → produces the List
4. Common Intermediate Operations
| Operation | Description | Example |
|---|---|---|
| filter(Predicate) | Keep elements that match condition | .filter(n -> n % 2 == 0) |
| map(Function) | Transform each element | .map(String::toUpperCase) |
| flatMap(Function) | Flatten nested structures | .flatMap(list -> list.stream()) |
| distinct() | Remove duplicates | .distinct() |
| sorted() | Sort (natural or Comparator) | .sorted() or .sorted(Comparator.reverseOrder()) |
| limit(n) | Take first n elements | .limit(5) |
| skip(n) | Skip first n elements | .skip(2) |
5. Common Terminal Operations
| Operation | Description | Return Type |
|---|---|---|
| forEach(Consumer) | Perform action on each element | void |
| collect(Collector) | Collect into List/Set/Map/String… | List, Set, Map, String… |
| reduce() | Combine elements into single result | Optional<T> |
| count() | Number of elements | long |
| anyMatch(Predicate) | Any element matches? | boolean |
| allMatch(), noneMatch() | All / none match | boolean |
| findFirst(), findAny() | First / any element | Optional<T> |
Collectors (most powerful):
|
0 1 2 3 4 5 6 7 8 9 |
// toList, toSet, joining, groupingBy, partitioningBy... List<String> list = stream.collect(Collectors.toList()); String joined = stream.collect(Collectors.joining(", ")); Map<String, List<String>> grouped = stream.collect(Collectors.groupingBy(String::length)); |
6. Parallel Streams (Easy Multi-threading)
Just add .parallel() — Java splits the work across multiple threads!
Example:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
List<Integer> numbers = new ArrayList<>(); for (int i = 1; i <= 1000000; i++) numbers.add(i); // Sequential long sumSeq = numbers.stream() .mapToLong(Integer::longValue) .sum(); // Parallel (usually much faster on multi-core CPU) long sumPar = numbers.parallelStream() .mapToLong(Integer::longValue) .sum(); System.out.println("Sequential sum: " + sumSeq); System.out.println("Parallel sum: " + sumPar); |
When to use parallel streams?
- Large datasets (10k+ elements)
- Expensive operations (filter/map)
- Not for small lists or I/O-bound tasks
Caution: Not all operations are thread-safe — be careful with stateful operations.
Quick Recap Table (Your Cheat Sheet)
| Concept | Key Points / Example | Benefit |
|---|---|---|
| Creating Stream | list.stream(), Stream.of(), Arrays.stream() | Source for pipeline |
| Intermediate | filter(), map(), sorted(), limit() | Transform data (lazy) |
| Terminal | collect(), forEach(), reduce(), count() | Produce result / side-effect |
| Parallel Stream | .parallelStream() or .parallel() | Automatic multi-threading |
| Collectors | toList(), joining(), groupingBy() | Powerful way to collect results |
Common Mistakes & Fixes
| Mistake | Problem | Fix |
|---|---|---|
| Using stream after terminal operation | IllegalStateException: stream has already been operated | Create new stream each time |
| Modifying collection inside forEach | ConcurrentModificationException | Collect to new list or use parallel carefully |
| Using parallel on small collections | Overhead > benefit | Use sequential for <10k elements |
| Forgetting to import Collectors | Cannot find symbol | import java.util.stream.Collectors; |
| Chaining too many operations | Hard to read | Break into smaller methods |
Homework for You (Practice to Master!)
- Basic: From a list of strings, filter names starting with “A”, convert to uppercase, sort, and collect to List.
- Medium: From list of integers 1 to 100, find sum of all even numbers using filter + mapToInt + sum().
- Advanced: Group a list of strings by their length using Collectors.groupingBy().
- Fun: Generate 10 random numbers using Stream.generate(Math::random).limit(10) and print them.
- Challenge: Find the first name longer than 6 characters from a list (use findFirst()).
You’re doing fantastic! Stream API + lambdas is the modern way to process data in Java — now your code looks clean, concise, and professional.
