Streams

Java Streams

A Stream is a pipeline through which data elements flow, letting you perform operations like filtering, mapping, sorting, and aggregation without writing explicit loops. It was introduced in Java 8 (package java.util.stream).

In simple words:

Stream = Data Source → Intermediate Operations → Terminal Operation

Real-Life Example: Factory Conveyor Belt

Think of a bottling factory:

text
1Raw bottles → Reject cracked ones → Fill with juice → Put on labels → Pack into boxes
2 (source) (filter) (map) (map) (terminal)
  • The machines (filter, fill, label) are set up along the belt, but nothing moves until the final packing station is switched on. This is lazy evaluation.
  • Bottles pass through the stations one by one, not all at once.
  • Once the batch is packed, the belt is finished. You cannot run the same batch through again. This is why a Stream cannot be reused.
  • The original crate of raw bottles is not changed. The output is a new set of boxes.

Stream vs Collection

FeatureCollectionStream
PurposeStores dataProcesses data
Data structureYesNo (it is a pipeline)
Modifies the sourceYes (add/remove)No
ReusableYesNo (one terminal operation, then closed)
EvaluationEagerLazy
Loop styleExternal iteration (you write loop)Internal iteration (Stream does it)
Can be infiniteNoYes (Stream.iterate, Stream.generate)

Stream Pipeline

text
1Data Source
2 ↓
3Create Stream
4 ↓
5Intermediate Operations (zero or more, lazy)
6 ↓
7Terminal Operation (exactly one, triggers execution)
8 ↓
9Result
StepRules
CreateFrom a collection, array, values, builder, or generator
IntermediateZero or more, return another Stream, lazy, can be chained
TerminalExactly one, triggers execution, produces result, closes the Stream

Example: Without Stream vs With Stream

Count salaries greater than 3000.

List<Integer> salaries = List.of(3000, 4000, 1000, 9000, 1000, 3500);
// Without Stream
int count = 0;
for (int salary : salaries) {
if (salary > 3000) {
count++;
}
}
System.out.println(count); // 3
// With Stream
long total = salaries.stream()
.filter(salary -> salary > 3000)
.count();
System.out.println(total); // 3
text
1salaries.stream() → filter() → count()
2 (create) (intermediate) (terminal)

The loop says how to do it. The Stream says what you want. This is the declarative style.

Ways to Create a Stream

SourceCode
Collectionlist.stream()
ArrayArrays.stream(arr)
Explicit valuesStream.of(1, 2, 3)
BuilderStream.builder().add(1).add(2).build()
Iterate (infinite)Stream.iterate(seed, fn)
Generate (infinite)Stream.generate(supplier)
Range of intsIntStream.range(1, 5), rangeClosed(1, 5)
import java.util.*;
import java.util.stream.*;
// 1. From a collection
Stream<Integer> s1 = List.of(1, 2, 3).stream();
// 2. From an array
int[] nums = {1, 2, 3, 4, 5};
IntStream s2 = Arrays.stream(nums);
String[] names = {"Akash", "Rahul", "Amit"};
Stream<String> s3 = Arrays.stream(names);
// 3. Stream.of()
Stream<Integer> s4 = Stream.of(1, 2, 3, 4, 5);
// 4. Stream.Builder
Stream.Builder<Integer> builder = Stream.builder();
builder.add(1);
builder.add(2);
builder.add(3);
Stream<Integer> s5 = builder.build();
// 5. Stream.iterate() with limit
Stream.iterate(1000, n -> n + 5000)
.limit(5)
.forEach(System.out::println);
// 1000, 6000, 11000, 16000, 21000
// 6. iterate with a stop condition (Java 9+)
Stream.iterate(1, n -> n <= 20, n -> n * 2)
.forEach(System.out::println);
// 1, 2, 4, 8, 16

Stream.iterate() and Stream.generate() can produce an infinite Stream. Always restrict them with limit() or a stop condition.

A Map has no stream() method. Stream its keySet(), values(), or entrySet() instead.

Intermediate Operations ⭐

Intermediate operations transform a Stream into another Stream. They are lazy: they do nothing until a terminal operation runs.

OperationPurposeFunctional interface
filter()Keep elements matching a conditionPredicate<T>
map()Transform each elementFunction<T, R>
flatMap()Transform and flatten nested streamsFunction<T, Stream<R>>
distinct()Remove duplicatesuses equals()/hashCode()
sorted()Sort elementsComparator<T>
peek()Observe elements (debugging)Consumer<T>
limit(n)Keep first n elementsnone
skip(n)Skip first n elementsnone
mapToInt() etc.Convert to a primitive StreamToIntFunction<T>
takeWhile() / dropWhile()Take or drop while a condition holds (Java 9+)Predicate<T>

filter()

Selects elements that satisfy a condition.

List<Integer> result = List.of(1, 2, 3, 4, 5, 6).stream()
.filter(n -> n > 3)
.toList();
System.out.println(result); // [4, 5, 6]
text
1Element → Predicate → true → keep
2 → false → discard

map()

Transforms each element into something else. The number of elements stays the same.

List<String> result = List.of("HELLO", "EVERYBODY", "JAVA").stream()
.map(String::toLowerCase)
.toList();
System.out.println(result); // [hello, everybody, java]
text
1filter → Which elements should remain?
2map → What should each element become?

flatMap() ⭐

Used for nested structures. It transforms each element into a Stream and then flattens all of them into one Stream.

List<List<String>> data = List.of(
List.of("I", "love", "Java"),
List.of("Streams", "are", "powerful")
);
List<String> result = data.stream()
.flatMap(List::stream)
.toList();
System.out.println(result);
// [I, love, Java, Streams, are, powerful]
text
1map: List<List<String>> → Stream<List<String>> (still nested)
2flatMap: List<List<String>> → Stream<String> (flattened)

flatMap = map + flatten

distinct()

Removes duplicates using equals() and hashCode().

List<Integer> result = List.of(1, 2, 2, 3, 3, 4, 4, 5).stream()
.distinct()
.toList();
System.out.println(result); // [1, 2, 3, 4, 5]

sorted()

List<Integer> numbers = List.of(5, 1, 9, 2, 4);
// Natural order
numbers.stream().sorted().toList(); // [1, 2, 4, 5, 9]
// Descending
numbers.stream().sorted(Comparator.reverseOrder()).toList(); // [9, 5, 4, 2, 1]
// Custom: sort strings by length
List.of("banana", "kiwi", "apple").stream()
.sorted(Comparator.comparing(String::length))
.toList(); // [kiwi, apple, banana]

Prefer Comparator.reverseOrder() over (a, b) -> b - a. The subtraction trick can overflow for large or negative values.

peek()

Performs an action on each element as it passes through. Mainly used for debugging.

List<Integer> result = List.of(1, 2, 3, 4, 5).stream()
.filter(n -> n > 2)
.peek(n -> System.out.println("Passed filter: " + n))
.toList();

peek() is lazy. Without a terminal operation, nothing is printed.

limit() and skip()

List<Integer> data = List.of(2, 1, 3, 4, 6);
data.stream().limit(3).toList(); // [2, 1, 3] (first 3)
data.stream().skip(3).toList(); // [4, 6] (skip first 3)
// Pagination: page 2, page size 2
data.stream().skip(2).limit(2).toList(); // [3, 4]

Primitive Streams ⭐

Streams of Integer use boxing (int ↔ Integer), which costs memory and time. Java provides specialized Streams for primitives.

StreamPrimitiveConvert using
IntStreamintmapToInt()
LongStreamlongmapToLong()
DoubleStreamdoublemapToDouble()

They also provide handy methods like sum(), average(), min(), max(), and summaryStatistics().

List<String> numbers = List.of("2", "1", "4", "7");
int[] arr = numbers.stream()
.mapToInt(Integer::parseInt)
.toArray();
int sum = IntStream.of(arr).sum(); // 14
double avg = IntStream.of(arr).average().orElse(0); // 3.5
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1 2 3 4 5
// Back to objects
Stream<Integer> boxed = IntStream.of(arr).boxed();

Lazy Evaluation ⭐⭐⭐

Intermediate operations do not run until a terminal operation is called.

List<Integer> numbers = List.of(2, 1, 4, 7, 10);
// No terminal operation: NOTHING is printed
numbers.stream()
.filter(n -> n >= 3)
.peek(System.out::println);
// With a terminal operation: pipeline runs
long count = numbers.stream()
.filter(n -> n >= 3)
.peek(System.out::println)
.count();
// Output:
// 4
// 7
// 10

Intermediate operations are lazy and are triggered by the terminal operation.

How Elements Flow Through the Pipeline

A common misconception is that each operation processes all elements before the next one starts. In reality, elements move through the pipeline one by one.

Stream.of(2, 1, 4, 7)
.filter(n -> {
System.out.println("filter " + n);
return n >= 3;
})
.map(n -> {
System.out.println("map " + n);
return n * 10;
})
.forEach(n -> System.out.println("result " + n));
// Output:
// filter 2
// filter 1
// filter 4
// map 4
// result 40
// filter 7
// map 7
// result 70
text
12 → filter ✗ (stops here)
21 → filter ✗ (stops here)
34 → filter ✓ → map → forEach
47 → filter ✓ → map → forEach

This is called vertical processing, and it is why a Stream does not need to create an intermediate collection after each step.

Stateful Operations

Some operations need to see all (or many) elements before they can produce output. sorted() is the classic example: it must collect everything before emitting the first element. It acts as a barrier in the pipeline, so elements before it are all processed before any element after it.

Short-Circuiting

Some operations can stop early once the answer is known, which is useful for large or infinite Streams.

TypeOperations
Intermediate short-circuitlimit(), takeWhile()
Terminal short-circuitanyMatch(), allMatch(), noneMatch(), findFirst(), findAny()
text
1Stream: 1, 2, 4, 7, 10, ... Condition: n > 3
2
3anyMatch(n -> n > 3): checks 1 ✗, 2 ✗, 4 ✓ → stops, returns true

Terminal Operations ⭐

Terminal operations trigger execution, produce the result, and close the Stream.

OperationPurposeReturns
forEach()Perform an action on each elementvoid
collect()Gather elements into a collectionCollection / result
toList()Gather into an unmodifiable list (Java 16+)List<T>
toArray()Convert to an arrayArray
reduce()Combine elements into one resultOptional / value
count()Count elementslong
min() / max()Smallest / largest elementOptional<T>
findFirst()First elementOptional<T>
findAny()Any elementOptional<T>
anyMatch()At least one matchesboolean
allMatch()All matchboolean
noneMatch()None matchboolean

forEach(), toArray(), count()

List<Integer> numbers = List.of(2, 1, 4, 7, 10);
// forEach
numbers.stream().filter(n -> n >= 3).forEach(System.out::println); // 4 7 10
// toArray
Object[] a = numbers.stream().toArray();
Integer[] b = numbers.stream().toArray(Integer[]::new); // typed array
// count
long count = numbers.stream().filter(n -> n >= 3).count(); // 3

reduce() ⭐⭐⭐

Combines all elements into a single result.

text
12, 1, 4, 7, 10
22 + 1 = 3
33 + 4 = 7
47 + 7 = 14
514 + 10 = 24
List<Integer> numbers = List.of(2, 1, 4, 7, 10);
// 1. Without identity: returns Optional (stream might be empty)
Optional<Integer> sum1 = numbers.stream().reduce((a, b) -> a + b);
System.out.println(sum1.get()); // 24
// 2. With identity: returns a plain value (identity is the result for an empty stream)
int sum2 = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 24
// Method reference
int sum3 = numbers.stream().reduce(0, Integer::sum);
// Multiplication
int product = List.of(2, 1, 4).stream().reduce(1, (a, b) -> a * b); // 8

The operation passed to reduce() must be associative (grouping must not change the result), so it works correctly with parallel Streams.

collect() and Collectors ⭐

collect() gathers Stream elements into a collection, string, or map using a Collector.

import java.util.stream.Collectors;
List<String> names = List.of("Ravi", "Anita", "Kiran", "Amit", "Ravi");
// To List / Set
List<String> list = names.stream().collect(Collectors.toList());
Set<String> set = names.stream().collect(Collectors.toSet());
// Joining into a String
String joined = names.stream().collect(Collectors.joining(", ", "[", "]"));
// [Ravi, Anita, Kiran, Amit, Ravi]
// To Map (keys must be unique, else IllegalStateException)
Map<String, Integer> lengths = names.stream().distinct()
.collect(Collectors.toMap(n -> n, String::length));
// Group by first letter
Map<Character, List<String>> byLetter = names.stream()
.collect(Collectors.groupingBy(n -> n.charAt(0)));
// {A=[Anita, Amit], K=[Kiran], R=[Ravi, Ravi]}
// Count per group
Map<String, Long> freq = names.stream()
.collect(Collectors.groupingBy(n -> n, Collectors.counting()));
// {Ravi=2, Anita=1, Kiran=1, Amit=1}
// Partition into two groups (true / false)
Map<Boolean, List<String>> parts = names.stream()
.collect(Collectors.partitioningBy(n -> n.length() > 4));
CollectorResult
toList(), toSet()List or Set
toMap(k, v)Map
joining()Single String
groupingBy()Map of groups
partitioningBy()Map with true / false keys
counting()Count (Long)
summingInt(), averagingInt()Sum / average

Stream.toList() (Java 16+) returns an unmodifiable list, while Collectors.toList() makes no guarantee about the list type.

min() and max()

Optional<Integer> min = List.of(4, 7, 10).stream().min(Integer::compareTo);
Optional<Integer> max = List.of(4, 7, 10).stream().max(Integer::compareTo);
System.out.println(min.get()); // 4
System.out.println(max.get()); // 10

anyMatch(), allMatch(), noneMatch()

List.of(1, 2, 4, 7).stream().anyMatch(n -> n > 3); // true (at least one)
List.of(2, 4, 6, 8).stream().allMatch(n -> n % 2 == 0); // true (all)
List.of(1, 3, 5).stream().noneMatch(n -> n % 2 == 0); // true (none)

On an empty Stream: anyMatch is false, while allMatch and noneMatch are true.

findFirst() and findAny()

Optional<Integer> first = List.of(4, 7, 10).stream().findFirst(); // 4
Optional<Integer> any = List.of(4, 7, 10).stream().findAny(); // any element

findFirst() always returns the first element. findAny() may return any element, which gives more freedom in parallel Streams.

Optional in Stream Results

min(), max(), findFirst(), findAny(), and the single-argument reduce() return an Optional, because the Stream might be empty.

Optional<Integer> result = numbers.stream().findFirst();
result.isPresent(); // true / false
result.get(); // value (throws if empty)
result.orElse(0); // value or default
result.ifPresent(System.out::println);

A Stream Cannot Be Reused ⭐⭐⭐

After a terminal operation the Stream is consumed.

Stream<Integer> stream = List.of(1, 2, 3, 4, 5).stream();
stream.count(); // terminal operation: stream is now closed
stream.filter(n -> n > 2); // ❌ IllegalStateException
text
1IllegalStateException: stream has already been operated upon or closed

Fix: create a new Stream from the source each time.

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream().count();
numbers.stream().filter(n -> n > 2).toList(); // ✅ new Stream

Sequential vs Parallel Streams

Featurestream()parallelStream()
ProcessingOne element at a time, one threadSplit into chunks, many threads
OrderMaintainedProcessing order not guaranteed (forEachOrdered() keeps it)
Best forMost everyday codeLarge data, CPU-heavy work
List<Integer> numbers = List.of(11, 12, 13, 14, 15);
numbers.parallelStream()
.map(n -> n * n)
.forEach(System.out::println); // order may vary
// Or convert an existing stream
numbers.stream().parallel();
text
1Collection
2 ↓
3Split into smaller tasks (Spliterator)
4 ↓
5Task 1 Task 2 Task 3 Task 4 ← run on multiple CPU cores
6 ↓
7Combine results (join)

Behind the Scenes

ConceptRole
Fork/Join frameworkParallel Streams run on the common ForkJoinPool: a big task is forked into smaller ones and the results are joined
SpliteratorTraverses the source and splits it into chunks using trySplit()

When NOT to Use Parallel Streams

SituationReason
Small collections / cheap operationsSplitting and merging cost more than the work
Operations with shared mutable stateRace conditions
Blocking I/O or database calls inside the pipelineBlocks the shared common pool
Order-dependent logicOrder is not preserved
Poorly splittable sources (e.g. LinkedList)Splitting is inefficient

Parallel does not automatically mean faster. Measure first.

Complete Pipeline Example ⭐⭐⭐

List<Integer> numbers = List.of(2, 1, 4, 7, 10);
List<Integer> result = numbers.stream()
.filter(n -> n >= 3)
.map(n -> -n)
.sorted()
.toList();
System.out.println(result); // [-10, -7, -4]
text
12, 1, 4, 7, 10
2 ↓ filter(n >= 3)
34, 7, 10
4 ↓ map(n -> -n)
5-4, -7, -10
6 ↓ sorted()
7-10, -7, -4
8 ↓ toList()
9[-10, -7, -4]

Real-World Example: Employees

record Employee(String name, String dept, double salary) { }
List<Employee> employees = List.of(
new Employee("Ravi", "IT", 80000),
new Employee("Anita", "HR", 50000),
new Employee("Kiran", "IT", 95000),
new Employee("Meena", "HR", 60000)
);
// 1. Names of IT employees earning more than 85000
List<String> names = employees.stream()
.filter(e -> e.dept().equals("IT"))
.filter(e -> e.salary() > 85000)
.map(Employee::name)
.toList(); // [Kiran]
// 2. Total salary
double total = employees.stream()
.mapToDouble(Employee::salary)
.sum(); // 285000.0
// 3. Highest paid employee
Optional<Employee> top = employees.stream()
.max(Comparator.comparingDouble(Employee::salary));
// 4. Average salary per department
Map<String, Double> avgByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.averagingDouble(Employee::salary)));
// {IT=87500.0, HR=55000.0}

Common Mistakes ⚠️

MistakeProblem / Fix
Forgetting the terminal operationNothing runs. Add toList(), count(), forEach(), etc.
Reusing a StreamIllegalStateException. Create a new Stream
Modifying the source collection inside the pipelineConcurrentModificationException. Collect into a new list
Relying on side effects in map() / filter()Keep lambdas pure (no external state changes)
Collectors.toMap() with duplicate keysIllegalStateException. Pass a merge function as the third argument
Calling get() on an empty OptionalNoSuchElementException. Use orElse() or ifPresent()
Using parallelStream() everywhereOften slower. Use only when measured
Using peek() for real logicIt is meant for debugging. Use forEach() or map()

Note: since Java 9, count() may skip the pipeline (and any peek()) if it can compute the size directly from the source, for example list.stream().peek(...).count() with no filter().

Functional Interfaces Used by Streams ⭐

OperationFunctional interfaceSignature
filter()Predicate<T>T → boolean
map()Function<T, R>T → R
flatMap()Function<T, Stream<R>>T → Stream<R>
peek()Consumer<T>T → void
forEach()Consumer<T>T → void
reduce()BinaryOperator<T>(T, T) → T
sorted()Comparator<T>(T, T) → int
generate()Supplier<T>() → T

Stream Cheat Sheet

text
1STREAM
2│
3├── Create
4│ ├── collection.stream()
5│ ├── Arrays.stream()
6│ ├── Stream.of()
7│ ├── Stream.builder()
8│ ├── Stream.iterate()
9│ └── Stream.generate()
10│
11├── Intermediate (lazy, return Stream)
12│ ├── filter() → select
13│ ├── map() → transform
14│ ├── flatMap() → flatten
15│ ├── distinct() → remove duplicates
16│ ├── sorted() → sort
17│ ├── peek() → observe
18│ ├── limit() → first N
19│ ├── skip() → skip N
20│ └── mapToInt() / mapToLong() / mapToDouble()
21│
22└── Terminal (triggers execution, closes Stream)
23 ├── forEach() ├── min() / max()
24 ├── collect() ├── findFirst() / findAny()
25 ├── toList() ├── anyMatch() / allMatch() / noneMatch()
26 ├── reduce() └── toArray()
27 └── count()

Interview Questions ⭐

Q1. What is a Stream? A pipeline for processing a sequence of elements from a source using operations like filter, map, and reduce. It does not store data.

Q2. Is a Stream a data structure? No. It does not store elements. It carries them from a source through operations.

Q3. What are the three parts of a Stream pipeline? Source, zero or more intermediate operations, and exactly one terminal operation.

Q4. Are intermediate operations eager or lazy? Lazy. They run only when a terminal operation is invoked.

Q5. Can a Stream have multiple terminal operations? No. The Stream is consumed after the first one.

Q6. Can a Stream be reused? No. Reuse throws IllegalStateException.

Q7. Does a Stream modify the original collection? No. It produces a new result and leaves the source unchanged.

Q8. Difference between map() and flatMap()? map() is one-to-one. flatMap() maps each element to a Stream and flattens them into one.

Q9. Difference between filter() and map()? filter() selects elements. map() transforms them.

Q10. Why does peek() sometimes print nothing? It is lazy. Without a terminal operation the pipeline never runs.

Q11. Difference between findFirst() and findAny()? findFirst() returns the first element. findAny() may return any element, which matters in parallel Streams.

Q12. Difference between stream() and parallelStream()? stream() is sequential. parallelStream() splits the work across multiple threads using the Fork/Join pool.

Q13. What is a Spliterator? An object that traverses a source and can split it (trySplit()) into chunks for parallel processing.

Q14. Difference between Collection and Stream? A Collection stores data and is reusable. A Stream processes data lazily and is single-use.

Q15. What is a short-circuiting operation? One that can stop early without processing all elements, such as limit(), anyMatch(), and findFirst().

Q16. Difference between collect() and reduce()? reduce() combines elements into one immutable-style value. collect() is a mutable reduction that accumulates into containers like a List or Map.

Interview Definition

A Java Stream is a lazily evaluated, single-use pipeline of operations (intermediate and terminal) that processes elements from a data source in a declarative, functional style without modifying the source.

Remember

Source → Intermediate (lazy, returns Stream) → Terminal (triggers, closes Stream). A Stream cannot be reused. parallelStream() is not always faster.

Next Topic Overview