Skip to main content

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โ€‹

FeatureCollectionStream
Stores data?โœ… YesโŒ No
Modifies source?โœ… CanโŒ Never
IterationExternal (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, count etc.) 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() on null โ†’ โœ… Fix: Always check for null before calling .stream(), or use Optional
  • โŒ Mistake: Forgetting that findFirst() returns Optional<T>, not T โ†’ โœ… Fix: Use .get(), .orElse(), or .ifPresent() to access the value

Best Practicesโ€‹

  • Prefer streams over manual for loops 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


  • Functional Interfaces
  • Lambda Expressions
  • Optional Class
  • Method References
  • Collectors API

Next Lessonโ€‹

Lesson 4 โ€” Optional Class