List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .filter(n -> n > 25) .forEach(System.out::println);Java Stream API: Filter, Map, Reduce, Collect, and Parallel Processing
Direct Answer & Core Concept
The Java Stream API provides a powerful way to process collections of data using a sequence of operations. Instead of writing many loops and temporary variables, you can describe what should happen to the data using operations such as filter(), map(), reduce(), and collect().
Streams are commonly used with List, Set, arrays, maps, and other data sources. They are especially useful when you need to filter records, transform values, calculate totals, group data, or build a new collection.
The Stream API was introduced in Java 8 and works closely with lambda expressions and functional interfaces.
Detailed Explanation & Mechanics
What Is a Java Stream?
A stream is a sequence of elements that can be processed through a pipeline of operations.
For example:
Output:
304050The stream did not change the original list. Instead, it processed the elements and produced the requested result.
A simple stream pipeline looks like:
Source↓filter()↓map()↓reduce()/collect()↓ResultWhy Use the Stream API?
Traditional collection processing often uses loops:
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
for (Integer number : numbers) {
if (number > 25) {
System.out.println(number);
}
}The same operation can be expressed with a stream:
numbers.stream() .filter(number -> number > 25) .forEach(System.out::println);The stream version separates the operations clearly:
filter → select elementsforEach → perform an actionThis can make data-processing pipelines easier to compose and understand.
Stream API Is Not a Data Structure
A stream is not a collection.
A collection stores data:
ListSetMapA stream processes data:
Collection → Stream → ResultFor example:
List<String> names = List.of("Amit", "Riya", "Rahul");
Stream<String> stream = names.stream();The List contains the elements. The Stream provides operations for processing them.
Creating a Stream From a Collection
The most common approach is:
List<String> names = List.of("Amit", "Riya", "Rahul");
names.stream();For a set:
Set<Integer> numbers = Set.of(10, 20, 30);
numbers.stream();For a map, you can stream its views:
Map<String, Integer> scores = Map.of( "Amit", 80, "Riya", 90);
scores.entrySet().stream();Creating a Stream From an Array
Use Arrays.stream():
int[] numbers = {
10, 20, 30, 40
};
Arrays.stream(numbers) .forEach(System.out::println);Output:
10203040You can also use:
Stream.of(...)for reference-type values.
Stream Pipeline
A stream pipeline normally contains three stages:
Source↓Intermediate Operations↓Terminal OperationExample:
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
int result = numbers.stream() .filter(n -> n > 15) .map(n -> n * 2) .reduce(0, Integer::sum);Here:
List↓stream()↓filter()↓map()↓reduce()↓int resultIntermediate Operations
Intermediate operations transform or process a stream and return another stream.
Common examples include:
filter()map()flatMap()distinct()sorted()limit()skip()peek()
For example:
numbers.stream() .filter(n -> n > 10) .map(n -> n * 2);This pipeline has not necessarily produced a final result yet because there is no terminal operation.
Terminal Operations
A terminal operation finishes a stream pipeline.
Common terminal operations include:
forEach()collect()reduce()count()min()max()findFirst()findAny()anyMatch()allMatch()noneMatch()
Example:
long count = numbers.stream() .filter(n -> n > 20) .count();The count() operation consumes the stream and produces a result.
Streams Are Usually Single-Use
Once a stream has been consumed by a terminal operation, it should not be reused.
For example:
Stream<Integer> stream = numbers.stream();
stream.count();
// stream.count(); // IllegalStateExceptionInstead, create a new stream:
numbers.stream().count();
numbers.stream().forEach(System.out::println);A collection can normally create multiple streams.
filter() — Selecting Elements
The filter() operation keeps elements that satisfy a condition.
Its basic idea is:
element → Predicate → true/falseExample:
List<Integer> numbers = List.of(5, 10, 15, 20, 25);
numbers.stream() .filter(n -> n >= 15) .forEach(System.out::println);Output:
152025Elements for which the predicate returns false are removed from the stream pipeline.
Filter Strings
List<String> names = List.of( "Amit", "Riya", "Rahul", "Anita");
names.stream() .filter(name -> name.startsWith("A")) .forEach(System.out::println);Output:
AmitAnitaMultiple Filters
You can use multiple filter() operations:
numbers.stream() .filter(n -> n > 10) .filter(n -> n % 2 == 0) .forEach(System.out::println);The pipeline keeps numbers that satisfy both conditions.
You can also combine conditions inside one predicate:
numbers.stream() .filter(n -> n > 10 && n % 2 == 0) .forEach(System.out::println);Both approaches can be useful depending on readability.
map() — Transforming Elements
The map() operation transforms each stream element into another value.
For example:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream() .map(n -> n * n) .toList();
System.out.println(squares);Output:
[1, 4, 9, 16, 25]Conceptually:
1 → 12 → 43 → 94 → 165 → 25Map One Type to Another
The input and output types do not have to be the same.
List<String> names = List.of("Amit", "Riya", "Rahul");
List<Integer> lengths = names.stream() .map(String::length) .toList();
System.out.println(lengths);Output:
[4, 4, 5]Here:
String → Integermap() With Objects
Suppose you have:
class Employee {
private String name;
private int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
} public String getName() {
return name;
} public int getSalary() {
return salary;
}
}You can extract employee names:
List<String> names = employees.stream() .map(Employee::getName) .toList();Or salaries:
List<Integer> salaries = employees.stream() .map(Employee::getSalary) .toList();flatMap() — Flattening Nested Data
map() creates one output for each input.
flatMap() is useful when each input can produce multiple values and you want one flattened stream.
Example:
List<List<Integer>> numbers = List.of( List.of(1, 2), List.of(3, 4), List.of(5, 6));Using flatMap():
List<Integer> result = numbers.stream() .flatMap(List::stream) .toList();
System.out.println(result);Output:
[1, 2, 3, 4, 5, 6]Conceptually:
[1,2] ─┐
[3,4] ─┼─→ [1,2,3,4,5,6]
[5,6] ─┘distinct() — Removing Duplicates
Use distinct() to remove duplicate elements according to their equality semantics.
List<Integer> numbers = List.of(10, 20, 20, 30, 10, 40);
List<Integer> unique = numbers.stream() .distinct() .toList();
System.out.println(unique);Output:
[10, 20, 30, 40]For custom objects, correct equals() and hashCode() implementations matter.
sorted() — Sorting Elements
You can sort stream elements:
List<Integer> numbers = List.of(50, 10, 30, 20, 40);
List<Integer> sorted = numbers.stream() .sorted() .toList();
System.out.println(sorted);Output:
[10, 20, 30, 40, 50]For custom objects, provide an appropriate comparator:
employees.stream() .sorted(Comparator.comparing(Employee::getSalary)) .forEach(System.out::println);Reverse Sorting
numbers.stream() .sorted(Comparator.reverseOrder()) .forEach(System.out::println);For objects:
employees.stream() .sorted( Comparator.comparing( Employee::getSalary).reversed()) .forEach(System.out::println);limit() — Taking the First Elements
The limit() operation restricts the number of elements.
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .limit(3) .forEach(System.out::println);Output:
102030skip() — Ignoring Elements
skip() ignores the first specified number of elements.
numbers.stream() .skip(2) .forEach(System.out::println);Output:
304050Combining skip() and limit() can create a simple slice:
numbers.stream() .skip(2) .limit(2) .forEach(System.out::println);Output:
3040reduce() — Combining Elements Into One Result
The reduce() operation combines stream elements into a single result.
For example, calculate a sum:
List<Integer> numbers = List.of(10, 20, 30, 40);
int sum = numbers.stream() .reduce(0,(a, b) -> a + b);
System.out.println(sum);Output:
100The operation can be visualized as:
0 + 10 = 1010 + 20 = 3030 + 30 = 6060 + 40 = 100Reduce With Method Reference
The previous example can be shortened:
int sum = numbers.stream() .reduce(0, Integer::sum);This is often easier to read.
Reduce Without an Identity
You can also use:
Optional<Integer> result = numbers.stream() .reduce(Integer::sum);The result is an Optional<Integer> because the stream might be empty.
For example:
List<Integer> numbers = List.of();
Optional<Integer> result = numbers.stream() .reduce(Integer::sum);There is no valid sum value unless you provide an identity such as 0.
Reduce for Maximum Value
Optional<Integer> maximum = numbers.stream() .reduce(Integer::max);
maximum.ifPresent(System.out::println);You can also use:
Optional<Integer> maximum = numbers.stream() .max(Integer::compareTo);The latter communicates the intent more directly.
Reduce for Multiplication
List<Integer> numbers = List.of(2, 3, 4);
int result = numbers.stream() .reduce(1,(a, b) -> a * b);
System.out.println(result);Output:
24The identity is 1 because multiplying by 1 does not change the result.
Choosing a Good Reduce Operation
Reduction operations should have appropriate combination behavior, especially when parallel processing is involved.
For example:
sumis naturally suitable because addition is associative:
(a + b) + c = a +(b + c)An operation that depends on a particular sequential order may not behave as expected when used with a parallel stream.
collect() — Building a Result
The collect() operation is commonly used to gather stream elements into a collection or another mutable result structure.
Example:
List<String> names = List.of("Amit", "Riya", "Rahul");
List<String> result = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());For modern Java, you can also write:
List<String> result = names.stream() .filter(name -> name.length() > 4) .toList();Collectors.toList()
Example:
List<Integer> result = numbers.stream() .filter(n -> n > 20) .collect(Collectors.toList());Collectors.toList() is useful when you specifically want the collector-based API.
Remember that Stream.toList() and Collectors.toList() are not identical in every mutability/detail aspect, so choose based on the required result behavior.
Collect Into a Set
Set<Integer> unique = numbers.stream() .collect(Collectors.toSet());This is useful when duplicate results should not be retained.
Collect Into a Map
Suppose:
List<Employee> employees = ...;You can create a map using:
Map<String, Integer> salaryMap = employees.stream() .collect( Collectors.toMap( Employee::getName, Employee::getSalary));This creates:
Employee name → SalaryBe careful when keys may not be unique. Collectors.toMap() requires a merge strategy when duplicate keys are possible.
For example:
Map<String, Integer> salaryMap = employees.stream() .collect( Collectors.toMap( Employee::getName, Employee::getSalary, Integer::max));If two employees produce the same key, the merge function decides which value to keep.
Grouping Data With groupingBy()
One of the most useful collectors is groupingBy().
Suppose employees have a department:
employees.stream() .collect( Collectors.groupingBy( Employee::getDepartment));The result is conceptually:
IT →[Employee, Employee]Finance →[Employee, Employee]HR →[Employee]This is useful for categorizing objects.
Counting With counting()
You can combine grouping with counting:
Map<String, Long> countByDepartment = employees.stream() .collect( Collectors.groupingBy( Employee::getDepartment, Collectors.counting()));The result might look like:
IT → 5Finance → 3HR → 2Joining Strings
The joining() collector combines strings.
List<String> names = List.of("Amit", "Riya", "Rahul");
String result = names.stream() .collect( Collectors.joining(", "));
System.out.println(result);Output:
Amit, Riya, RahulYou can also specify prefix and suffix:
String result = names.stream() .collect( Collectors.joining( ", ", "[", "]"));Output:
[Amit, Riya, Rahul]forEach() — Performing an Action
The forEach() operation applies an action to each element.
numbers.stream() .forEach(System.out::println);It is often useful for displaying or sending data somewhere.
However, avoid using forEach() as a replacement for every loop. If you are building a result, map(), collect(), or another appropriate terminal operation may express the intent better.
findFirst() and findAny()
findFirst() returns the first element according to the stream's encounter order when one exists.
Optional<Integer> first = numbers.stream() .filter(n -> n > 20) .findFirst();findAny() returns some matching element.
Optional<Integer> result = numbers.parallelStream() .filter(n -> n > 20) .findAny();findAny() can be particularly useful when encounter order does not matter.
anyMatch(), allMatch(), and noneMatch()
These operations test stream elements.
anyMatch()
Returns true if at least one element matches:
boolean result = numbers.stream() .anyMatch(n -> n > 100);allMatch()
Returns true if all elements match:
boolean result = numbers.stream() .allMatch(n -> n > 0);noneMatch()
Returns true if no elements match:
boolean result = numbers.stream() .noneMatch(n -> n < 0);These operations can short-circuit without processing every element.
Lazy Evaluation
Many stream intermediate operations are lazy.
For example:
Stream<Integer> stream = numbers.stream() .filter(n -> {
System.out.println("Testing " + n);
return n > 20;
});The filter may not execute immediately because there is no terminal operation.
Add:
stream.forEach(System.out::println);and the pipeline executes.
Conceptually:
Build pipeline↓No immediate processing↓Terminal operation↓Pipeline executesStream Operations Can Be Fused
Consider:
numbers.stream() .filter(n -> n > 10) .map(n -> n * 2) .filter(n -> n < 100) .forEach(System.out::println);A stream implementation can process elements through the pipeline without necessarily creating a separate collection after every intermediate operation.
This is one reason stream pipelines can be expressive and efficient.
Short-Circuiting Operations
Some stream operations can stop processing once the result is known.
Examples include:
findFirst()findAny()anyMatch()allMatch()noneMatch()limit()For example:
boolean found = numbers.stream() .anyMatch(n -> n > 100);Once a matching element is found, the stream does not need to continue searching.
Primitive Streams
Java provides specialized streams for primitive values:
IntStreamLongStreamDoubleStreamFor example:
int sum = IntStream.of(10, 20, 30, 40) .sum();
System.out.println(sum);Output:
100Primitive streams can avoid unnecessary boxing and provide useful numeric operations such as:
sum()average()min()max()summaryStatistics()mapToInt()
Suppose you have employee objects:
int totalSalary = employees.stream() .mapToInt(Employee::getSalary) .sum();This converts the object stream into an IntStream.
Conceptually:
Employee↓getSalary()↓int↓IntStream↓sum()mapToLong() and mapToDouble()
Similar operations exist for other primitive types:
long total = employees.stream() .mapToLong(Employee::getLongValue) .sum();and:
double average = employees.stream() .mapToDouble(Employee::getSalary) .average() .orElse(0.0);Parallel Streams
A stream can be processed in parallel:
numbers.parallelStream() .filter(n -> n > 20) .forEach(System.out::println);Alternatively:
numbers.stream() .parallel() .filter(n -> n > 20) .forEach(System.out::println);Parallel streams allow stream operations to be executed across multiple threads where the implementation determines that parallel execution is appropriate.
Sequential vs Parallel Stream
Sequential:
numbers.stream() .map(n -> n * 2) .toList();Parallel:
numbers.parallelStream() .map(n -> n * 2) .toList();The syntax change is small, but the execution model is different.
Parallel Streams Do Not Always Mean Faster
A common mistake is assuming:
parallelStream() = fasterThat is not guaranteed.
Parallel processing introduces overhead for:
Splitting the data
Scheduling work
Coordinating tasks
Combining results
Managing shared resources
For a small collection, sequential processing may be faster.
Parallel streams can be more useful when the workload is sufficiently large and each operation is suitable for parallel execution.
Avoid Shared Mutable State
This is dangerous:
List<Integer> result = new ArrayList<>();
numbers.parallelStream() .forEach(result::add);Multiple threads may attempt to modify the same ArrayList.
Instead, use a proper collector:
List<Integer> result = numbers.parallelStream() .map(n -> n * 2) .toList();The stream framework can handle the collection process appropriately.
Ordering With Parallel Streams
Consider:
numbers.parallelStream() .forEach(System.out::println);Do not assume the output will appear in the original encounter order.
If preserving encounter order matters, forEachOrdered() can be used:
numbers.parallelStream() .forEachOrdered(System.out::println);Ordering can reduce some opportunities for parallel execution, so use it when ordering is actually required.
Parallel reduce()
Reduction operations need to be designed appropriately for parallel execution.
For example:
int sum = numbers.parallelStream() .reduce(0, Integer::sum);Addition is suitable because the combination is associative.
Avoid designing a reduction around assumptions that depend on a specific sequential processing order.
Parallel Stream Common Mistakes
Mistake 1: Using Parallel Streams Everywhere
Do not automatically replace:
stream()with:
parallelStream()Measure and verify performance for the actual workload.
Mistake 2: Shared Mutable Variables
Avoid:
int total = 0;
numbers.parallelStream() .forEach(n -> total += n);This is not a valid way to safely accumulate shared state.
Use:
int total = numbers.parallelStream() .mapToInt(Integer::intValue) .sum();Mistake 3: Assuming Output Order
Parallel execution does not guarantee the same observable order for every terminal operation.
Complete Stream Example
Consider:
List<Integer> salaries = List.of( 25000, 45000, 60000, 75000, 90000);Find salaries of at least ₹50,000, increase them by ₹5,000, and collect the result:
List<Integer> updated = salaries.stream() .filter(salary -> salary >= 50000) .map(salary -> salary + 5000) .toList();
System.out.println(updated);Output:
[65000, 80000, 95000]The pipeline is:
Original salaries↓filter()↓50,000+↓map()↓+5,000↓toList()↓New listReal-World Example: Employee Processing
Suppose an application needs to find the names of employees earning more than ₹60,000.
List<String> highEarners = employees.stream() .filter(e -> e.getSalary() > 60000) .map(Employee::getName) .toList();This pipeline clearly expresses the business operation:
Employees↓salary > 60000↓employee name↓List<String>Stream API vs Traditional Loop
Traditional approach:
List<String> result = new ArrayList<>();
for (Employee employee : employees) {
if (employee.getSalary() > 60000) {
result.add(employee.getName());
}
}Stream approach:
List<String> result = employees.stream() .filter(e -> e.getSalary() > 60000) .map(Employee::getName) .toList();The stream version can be more declarative: it describes what data transformation is required rather than explicitly managing the loop and result list.
However, a traditional loop can still be clearer for complex stateful logic. Streams are a tool, not a requirement.
When Should You Use Streams?
Streams are particularly useful for:
Filtering collections
Transforming objects
Calculating totals
Finding matching elements
Sorting data
Grouping records
Building result collections
Chaining multiple data-processing steps
Processing large datasets when operations are suitable
Writing concise collection-processing code
When Should You Avoid Streams?
A traditional loop may be easier to understand when:
The logic requires many mutable variables
You need complex control flow
You need frequent
breakorcontinuebehaviorThe operation is highly stateful
A stream pipeline would become excessively long
Debugging the stream pipeline would be harder than a straightforward loop
Good Java code is not about using streams everywhere. It is about choosing the clearest approach for the problem.
Best Practices
Keep Pipelines Readable
Avoid extremely long chains.
Instead of putting everything into one expression, break complex logic into meaningful methods.
Prefer Stateless Operations
Operations such as:
filter()map()are easiest to reason about when they do not depend on changing shared state.
Avoid Side Effects
Prefer:
List<Integer> result = numbers.stream() .map(n -> n * 2) .toList();over modifying external state inside forEach().
Use Primitive Streams for Numeric Work
For numeric calculations, consider:
IntStreamLongStreamDoubleStreamwhen appropriate.
Use Parallel Streams Carefully
Parallelism should be based on the workload and measured performance rather than simply the size of the code.
Use the Most Expressive Operation
For example, use:
anyMatch()when you need to know whether any element matches rather than collecting all matches just to check whether the result is non-empty.
Frequently Asked Questions
What is the Java Stream API?
The Stream API provides a way to process sequences of data using operations such as filtering, transformation, reduction, sorting, grouping, and collection.
Is a Java Stream the same as a collection?
No. A collection stores data, while a stream represents a pipeline for processing data.
What does filter() do?
filter() keeps elements that satisfy a predicate.
numbers.stream() .filter(n -> n > 10);What does map() do?
map() transforms each element into another value.
numbers.stream() .map(n -> n * 2);What does reduce() do?
reduce() combines stream elements into a single result, such as a sum or product.
int sum = numbers.stream() .reduce(0, Integer::sum);What does collect() do?
collect() gathers stream results into a collection or another result structure using a collector.
List<Integer> result = numbers.stream() .collect(Collectors.toList());Can a stream be reused?
No. Once a stream has been consumed by a terminal operation, it cannot normally be reused. Create another stream from the source.
What is a parallel stream?
A parallel stream allows stream processing to use multiple threads where appropriate.
numbers.parallelStream() .forEach(System.out::println);Is parallelStream() always faster?
No. Parallel processing has overhead and may be slower for small datasets or unsuitable workloads.
What is the difference between map() and flatMap()?
map() transforms each element independently. flatMap() is useful when each element produces a nested stream and those streams need to be flattened into one stream.
What is the difference between forEach() and collect()?
forEach() performs an action on each element. collect() is generally used to produce a result such as a list, set, map, grouped structure, or another accumulated result.
What is a short-circuiting stream operation?
It is an operation that may stop processing before every element has been examined.
Examples include:
findFirst()findAny()anyMatch()allMatch()noneMatch()limit()Can streams modify the original collection?
Stream operations generally do not modify the source collection automatically. However, lambdas used within a stream can perform side effects, so avoid mutating shared state unless there is a clear reason and the operation is safe.
Graphics and Design Ideas
Graphic 1 — Java Stream Pipeline
┌────────────┐
│ Collection │
└─────┬──────┘
↓
┌────────────┐
│ stream() │
└─────┬──────┘
↓
┌────────────┐
│ filter() │
└─────┬──────┘
↓
┌────────────┐
│ map() │
└─────┬──────┘
↓
┌────────────┐
│ reduce() │
│ or │
│ collect() │
└─────┬──────┘
↓
┌────────────┐
│ Result │
└────────────┘This should be the main hero graphic because it immediately explains the idea of a stream pipeline.
Graphic 2 — Filter vs Map vs Reduce
FILTER[10, 20, 30, 40]↓n > 20↓[30, 40]MAP[10, 20, 30]↓n × 2↓[20, 40, 60]REDUCE[10, 20, 30]↓SUM↓60This visual gives beginners an instant understanding of the three most important stream operations.
Graphic 3 — map() vs flatMap()
map()
[1, 2] [3, 4] [5, 6]
↓
↓
↓
Stream Stream Stream
↓
↓
↓
Nested result
flatMap()
[1, 2] [3, 4] [5, 6]
└────────┼────────┘
↓
[1, 2, 3, 4, 5, 6]Graphic 4 — Sequential vs Parallel Stream
SEQUENTIAL
Data
│
├── Item 1
├── Item 2
├── Item 3
├── Item 4
│
↓
One processing flow
PARALLEL
Data
│
┌──────┼──────┐
↓
↓
↓
Worker Worker Worker
│ │ │
└──────┼──────┘
↓
ResultAdd a note:
Parallel does not automatically mean faster.
Graphic 5 — Intermediate vs Terminal Operations
INTERMEDIATE
─────────────
filter()
map()
flatMap()
sorted()
distinct()
limit()
↓
TERMINAL
─────────
collect()
reduce()
forEach()
count()
findFirst()
anyMatch()This visual is useful because understanding this distinction is one of the first major steps toward mastering Java Streams.
Runnable Code Example: Java Stream API
Java 17+import java.util.List;
import java.util.stream.Collectors;
public class StreamApiDemo {
public static void main(String[] args) {
List<String> cities = List.of("New York", "London", "Tokyo", "Paris", "San Francisco");
List<String> result = cities.stream() .filter(city -> city.length() > 5) .map(String::toUpperCase) .sorted() .collect(Collectors.toList());
System.out.println("Filtered Cities: " + result);
}
}