Topic 118 of 119 14. Additional Java Topics Interactive Tutorial

Java Stream API: Filter, Map, Reduce, Collect, and Parallel Processing

9 mins read Last updated: 22 Aug 2026 17,414 views

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:

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .filter(n -> n > 25) .forEach(System.out::println);

Output:

The stream did not change the original list. Instead, it processed the elements and produced the requested result.

A simple stream pipeline looks like:

Main.java
Run Online
Source↓filter()↓map()↓reduce()/collect()↓Result

Why Use the Stream API?

Traditional collection processing often uses loops:

Main.java
Run Online
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:

Main.java
Run Online
numbers.stream() .filter(number -> number > 25) .forEach(System.out::println);

The stream version separates the operations clearly:

Main.java
Run Online
filter → select elementsforEach → perform an action

This 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:

A stream processes data:

Main.java
Run Online
Collection → Stream → Result

For example:

Main.java
Run Online
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:

Main.java
Run Online
List<String> names = List.of("Amit", "Riya", "Rahul");
names.stream();

For a set:

Main.java
Run Online
Set<Integer> numbers = Set.of(10, 20, 30);
numbers.stream();

For a map, you can stream its views:

Main.java
Run Online
Map<String, Integer> scores = Map.of( "Amit", 80, "Riya", 90);
scores.entrySet().stream();

Creating a Stream From an Array

Use Arrays.stream():

Main.java
Run Online
int[] numbers = {
    10, 20, 30, 40
};
Arrays.stream(numbers) .forEach(System.out::println);

Output:

You can also use:

for reference-type values.


Stream Pipeline

A stream pipeline normally contains three stages:

Main.java
Run Online
Source↓Intermediate Operations↓Terminal Operation

Example:

Main.java
Run Online
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:

Main.java
Run Online
List↓stream()↓filter()↓map()↓reduce()↓int result

Intermediate 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:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
Stream<Integer> stream = numbers.stream();
stream.count();
// stream.count(); // IllegalStateException

Instead, create a new stream:

Main.java
Run Online
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:

Main.java
Run Online
element → Predicate → true/false

Example:

Main.java
Run Online
List<Integer> numbers = List.of(5, 10, 15, 20, 25);
numbers.stream() .filter(n -> n >= 15) .forEach(System.out::println);

Output:

Elements for which the predicate returns false are removed from the stream pipeline.


Filter Strings

Main.java
Run Online
List<String> names = List.of( "Amit", "Riya", "Rahul", "Anita");
names.stream() .filter(name -> name.startsWith("A")) .forEach(System.out::println);

Output:


Multiple Filters

You can use multiple filter() operations:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
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:

Conceptually:

Main.java
Run Online
1 → 12 → 43 → 94 → 165 → 25

Map One Type to Another

The input and output types do not have to be the same.

Main.java
Run Online
List<String> names = List.of("Amit", "Riya", "Rahul");
List<Integer> lengths = names.stream() .map(String::length) .toList();
System.out.println(lengths);

Output:

Here:


map() With Objects

Suppose you have:

Main.java
Run Online
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:

Main.java
Run Online
List<String> names = employees.stream() .map(Employee::getName) .toList();

Or salaries:

Main.java
Run Online
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:

Main.java
Run Online
List<List<Integer>> numbers = List.of( List.of(1, 2), List.of(3, 4), List.of(5, 6));

Using flatMap():

Main.java
Run Online
List<Integer> result = numbers.stream() .flatMap(List::stream) .toList();
System.out.println(result);

Output:

Conceptually:

Architecture Diagram
[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.

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 20, 30, 10, 40);
List<Integer> unique = numbers.stream() .distinct() .toList();
System.out.println(unique);

Output:

For custom objects, correct equals() and hashCode() implementations matter.


sorted() — Sorting Elements

You can sort stream elements:

Main.java
Run Online
List<Integer> numbers = List.of(50, 10, 30, 20, 40);
List<Integer> sorted = numbers.stream() .sorted() .toList();
System.out.println(sorted);

Output:

For custom objects, provide an appropriate comparator:

Main.java
Run Online
employees.stream() .sorted(Comparator.comparing(Employee::getSalary)) .forEach(System.out::println);

Reverse Sorting

Main.java
Run Online
numbers.stream() .sorted(Comparator.reverseOrder()) .forEach(System.out::println);

For objects:

Main.java
Run Online
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.

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .limit(3) .forEach(System.out::println);

Output:


skip() — Ignoring Elements

skip() ignores the first specified number of elements.

Main.java
Run Online
numbers.stream() .skip(2) .forEach(System.out::println);

Output:

Combining skip() and limit() can create a simple slice:

Main.java
Run Online
numbers.stream() .skip(2) .limit(2) .forEach(System.out::println);

Output:


reduce() — Combining Elements Into One Result

The reduce() operation combines stream elements into a single result.

For example, calculate a sum:

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 30, 40);
int sum = numbers.stream() .reduce(0,(a, b) -> a + b);
System.out.println(sum);

Output:

The operation can be visualized as:

Main.java
Run Online
0 + 10 = 1010 + 20 = 3030 + 30 = 6060 + 40 = 100

Reduce With Method Reference

The previous example can be shortened:

Main.java
Run Online
int sum = numbers.stream() .reduce(0, Integer::sum);

This is often easier to read.


Reduce Without an Identity

You can also use:

Main.java
Run Online
Optional<Integer> result = numbers.stream() .reduce(Integer::sum);

The result is an Optional<Integer> because the stream might be empty.

For example:

Main.java
Run Online
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

Main.java
Run Online
Optional<Integer> maximum = numbers.stream() .reduce(Integer::max);
maximum.ifPresent(System.out::println);

You can also use:

Main.java
Run Online
Optional<Integer> maximum = numbers.stream() .max(Integer::compareTo);

The latter communicates the intent more directly.


Reduce for Multiplication

Main.java
Run Online
List<Integer> numbers = List.of(2, 3, 4);
int result = numbers.stream() .reduce(1,(a, b) -> a * b);
System.out.println(result);

Output:

The 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:

is naturally suitable because addition is associative:

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:

Main.java
Run Online
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:

Main.java
Run Online
List<String> result = names.stream() .filter(name -> name.length() > 4) .toList();

Collectors.toList()

Example:

Main.java
Run Online
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

Main.java
Run Online
Set<Integer> unique = numbers.stream() .collect(Collectors.toSet());

This is useful when duplicate results should not be retained.


Collect Into a Map

Suppose:

Main.java
Run Online
List<Employee> employees = ...;

You can create a map using:

Main.java
Run Online
Map<String, Integer> salaryMap = employees.stream() .collect( Collectors.toMap( Employee::getName, Employee::getSalary));

This creates:

Be careful when keys may not be unique. Collectors.toMap() requires a merge strategy when duplicate keys are possible.

For example:

Main.java
Run Online
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:

Main.java
Run Online
employees.stream() .collect( Collectors.groupingBy( Employee::getDepartment));

The result is conceptually:

Main.java
Run Online
IT →[Employee, Employee]Finance →[Employee, Employee]HR →[Employee]

This is useful for categorizing objects.


Counting With counting()

You can combine grouping with counting:

Main.java
Run Online
Map<String, Long> countByDepartment = employees.stream() .collect( Collectors.groupingBy( Employee::getDepartment, Collectors.counting()));

The result might look like:

Main.java
Run Online
IT → 5Finance → 3HR → 2

Joining Strings

The joining() collector combines strings.

Main.java
Run Online
List<String> names = List.of("Amit", "Riya", "Rahul");
String result = names.stream() .collect( Collectors.joining(", "));
System.out.println(result);

Output:

You can also specify prefix and suffix:

Main.java
Run Online
String result = names.stream() .collect( Collectors.joining( ", ", "[", "]"));

Output:


forEach() — Performing an Action

The forEach() operation applies an action to each element.

Main.java
Run Online
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.

Main.java
Run Online
Optional<Integer> first = numbers.stream() .filter(n -> n > 20) .findFirst();

findAny() returns some matching element.

Main.java
Run Online
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:

Main.java
Run Online
boolean result = numbers.stream() .anyMatch(n -> n > 100);

allMatch()

Returns true if all elements match:

Main.java
Run Online
boolean result = numbers.stream() .allMatch(n -> n > 0);

noneMatch()

Returns true if no elements match:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
stream.forEach(System.out::println);

and the pipeline executes.

Conceptually:

Main.java
Run Online
Build pipeline↓No immediate processing↓Terminal operation↓Pipeline executes

Stream Operations Can Be Fused

Consider:

Main.java
Run Online
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:

Main.java
Run Online
findFirst()findAny()anyMatch()allMatch()noneMatch()limit()

For example:

Main.java
Run Online
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:

Main.java
Run Online
IntStreamLongStreamDoubleStream

For example:

Main.java
Run Online
int sum = IntStream.of(10, 20, 30, 40) .sum();
System.out.println(sum);

Output:

Primitive streams can avoid unnecessary boxing and provide useful numeric operations such as:

Main.java
Run Online
sum()average()min()max()summaryStatistics()

mapToInt()

Suppose you have employee objects:

Main.java
Run Online
int totalSalary = employees.stream() .mapToInt(Employee::getSalary) .sum();

This converts the object stream into an IntStream.

Conceptually:

Main.java
Run Online
Employee↓getSalary()↓int↓IntStream↓sum()

mapToLong() and mapToDouble()

Similar operations exist for other primitive types:

Main.java
Run Online
long total = employees.stream() .mapToLong(Employee::getLongValue) .sum();

and:

Main.java
Run Online
double average = employees.stream() .mapToDouble(Employee::getSalary) .average() .orElse(0.0);

Parallel Streams

A stream can be processed in parallel:

Main.java
Run Online
numbers.parallelStream() .filter(n -> n > 20) .forEach(System.out::println);

Alternatively:

Main.java
Run Online
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:

Main.java
Run Online
numbers.stream() .map(n -> n * 2) .toList();

Parallel:

Main.java
Run Online
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:

Main.java
Run Online
parallelStream() = faster

That 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:

Main.java
Run Online
List<Integer> result = new ArrayList<>();
numbers.parallelStream() .forEach(result::add);

Multiple threads may attempt to modify the same ArrayList.

Instead, use a proper collector:

Main.java
Run Online
List<Integer> result = numbers.parallelStream() .map(n -> n * 2) .toList();

The stream framework can handle the collection process appropriately.


Ordering With Parallel Streams

Consider:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
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:

with:

Measure and verify performance for the actual workload.

Mistake 2: Shared Mutable Variables

Avoid:

Main.java
Run Online
int total = 0;
numbers.parallelStream() .forEach(n -> total += n);

This is not a valid way to safely accumulate shared state.

Use:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
List<Integer> updated = salaries.stream() .filter(salary -> salary >= 50000) .map(salary -> salary + 5000) .toList();
System.out.println(updated);

Output:

The pipeline is:

Main.java
Run Online
Original salaries↓filter()↓50,000+↓map()↓+5,000↓toList()↓New list

Real-World Example: Employee Processing

Suppose an application needs to find the names of employees earning more than ₹60,000.

Main.java
Run Online
List<String> highEarners = employees.stream() .filter(e -> e.getSalary() > 60000) .map(Employee::getName) .toList();

This pipeline clearly expresses the business operation:

Main.java
Run Online
Employees↓salary > 60000↓employee name↓List<String>

Stream API vs Traditional Loop

Traditional approach:

Main.java
Run Online
List<String> result = new ArrayList<>();
for (Employee employee : employees) {
    if (employee.getSalary() > 60000) {
        result.add(employee.getName());
    }
}

Stream approach:

Main.java
Run Online
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 break or continue behavior

  • The 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:

are easiest to reason about when they do not depend on changing shared state.

Avoid Side Effects

Prefer:

Main.java
Run Online
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:

Main.java
Run Online
IntStreamLongStreamDoubleStream

when 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:

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.

Main.java
Run Online
numbers.stream() .filter(n -> n > 10);

What does map() do?

map() transforms each element into another value.

Main.java
Run Online
numbers.stream() .map(n -> n * 2);

What does reduce() do?

reduce() combines stream elements into a single result, such as a sum or product.

Main.java
Run Online
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.

Main.java
Run Online
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.

Main.java
Run Online
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:

Main.java
Run Online
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

Architecture Diagram
┌────────────┐
│ 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

Main.java
Run Online
FILTER[10, 20, 30, 40]↓n > 20↓[30, 40]MAP[10, 20, 30]↓n × 2↓[20, 40, 60]REDUCE[10, 20, 30]↓SUM↓60

This visual gives beginners an instant understanding of the three most important stream operations.


Graphic 3 — map() vs flatMap()

Architecture Diagram
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

Architecture Diagram
SEQUENTIAL

Data
 │
 ├── Item 1
 ├── Item 2
 ├── Item 3
 ├── Item 4
 │
↓
One processing flow


PARALLEL

             Data
              │
       ┌──────┼──────┐
↓

↓

↓
Worker  Worker  Worker
       │      │      │
       └──────┼──────┘
↓
Result

Add a note:

Parallel does not automatically mean faster.


Graphic 5 — Intermediate vs Terminal Operations

Architecture Diagram
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+
Main.java
Run Online
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);
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What is the difference between intermediate and terminal operations in Streams?

Intermediate operations are lazy and return a new Stream without executing; terminal operations trigger pipeline execution and produce a final non-stream result.
Q2

When should you avoid using parallelStream()?

For small datasets, I/O-bound tasks, or when operations modify shared mutable state, as thread coordination overhead outweighs parallel speedups.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Keep stream operations stateless and non-interfering, and collect results into immutable lists using toList() (Java 16+).