Stream API in Java
Learn the Java Stream API for processing collections and data using filtering, mapping, sorting, reducing, grouping, matching, and collecting operations.
What is Stream API?
The Stream API provides a functional-style approach for processing sequences of data.
A stream does not store data. Instead, it represents a pipeline of operations performed on a data source such as a collection, array, or generated sequence.
Data Source
↓
Stream
↓
Intermediate Operations
↓
Terminal Operation
↓
Result
Creating a Stream
List<String> names =
List.of(
"Amit",
"Neha",
"Rahul"
);
names.stream()
.forEach(
name ->
System.out.println(name)
);
Stream Pipeline
A stream pipeline normally contains a source, zero or more intermediate operations, and one terminal operation.
numbers.stream()
.filter(number -> number > 10)
.map(number -> number * 2)
.forEach(
System.out::println
);
-
stream()creates the stream. -
filter()selects elements. -
map()transforms elements. -
forEach()consumes the result.
Intermediate Operations
Intermediate operations transform or refine a stream and return another stream.
| Operation | Purpose |
|---|---|
filter() |
Selects elements matching a condition. |
map() |
Transforms each element. |
flatMap() |
Flattens nested stream structures. |
distinct() |
Removes duplicate elements. |
sorted() |
Sorts elements. |
limit() |
Restricts the number of elements. |
skip() |
Skips the first elements. |
peek() |
Allows an action for debugging/observation. |
Terminal Operations
Terminal operations produce a result or side effect and consume the stream.
| Operation | Purpose |
|---|---|
forEach() |
Performs an action for each element. |
collect() |
Collects results into a data structure. |
reduce() |
Combines elements into a single result. |
count() |
Counts elements. |
min() |
Finds the minimum element. |
max() |
Finds the maximum element. |
findFirst() |
Returns the first element if present. |
anyMatch() |
Checks whether any element matches. |
filter()
The filter() operation keeps elements that satisfy
a condition.
List<Integer> numbers =
List.of(
10,
15,
20,
25,
30
);
numbers.stream()
.filter(
number -> number > 20
)
.forEach(
System.out::println
);
map()
The map() operation transforms each stream
element into another value.
List<Integer> numbers =
List.of(1, 2, 3, 4, 5);
numbers.stream()
.map(
number -> number * number
)
.forEach(
System.out::println
);
distinct()
The distinct() operation removes duplicate
elements according to the stream elements' equality.
List<Integer> numbers =
List.of(
10,
20,
10,
30,
20
);
numbers.stream()
.distinct()
.forEach(
System.out::println
);
sorted()
The sorted() operation returns elements in sorted
order.
List<Integer> numbers =
List.of(
50,
10,
40,
20,
30
);
numbers.stream()
.sorted()
.forEach(
System.out::println
);
Sorting with Comparator
List<String> names =
List.of(
"Amit",
"Neha",
"Rahul",
"Priya"
);
names.stream()
.sorted(
(a, b) ->
b.compareTo(a)
)
.forEach(
System.out::println
);
limit()
The limit() operation restricts the stream to a
maximum number of elements.
List<Integer> numbers =
List.of(
10,
20,
30,
40,
50
);
numbers.stream()
.limit(3)
.forEach(
System.out::println
);
skip()
The skip() operation discards the first specified
number of elements.
numbers.stream()
.skip(2)
.forEach(
System.out::println
);
flatMap()
flatMap() is useful when each element produces
another collection or stream and you want one flattened
stream.
List<List<String>> groups =
List.of(
List.of("A", "B"),
List.of("C", "D")
);
groups.stream()
.flatMap(
group -> group.stream()
)
.forEach(
System.out::println
);
reduce()
The reduce() operation combines stream elements
into a single result.
List<Integer> numbers =
List.of(10, 20, 30, 40);
int sum =
numbers.stream()
.reduce(
0,
(a, b) -> a + b
);
System.out.println(sum);
count()
long count =
numbers.stream()
.filter(
number -> number > 20
)
.count();
System.out.println(count);
min() and max()
Optional<Integer> minimum =
numbers.stream()
.min(Integer::compareTo);
Optional<Integer> maximum =
numbers.stream()
.max(Integer::compareTo);
Matching Operations
Streams provide matching operations for checking conditions across elements.
boolean any =
numbers.stream()
.anyMatch(
number -> number > 30
);
boolean all =
numbers.stream()
.allMatch(
number -> number > 0
);
boolean none =
numbers.stream()
.noneMatch(
number -> number < 0
);
| Operation | Meaning |
|---|---|
anyMatch() |
At least one element matches. |
allMatch() |
Every element matches. |
noneMatch() |
No element matches. |
findFirst() and findAny()
Optional<Integer> first =
numbers.stream()
.filter(
number -> number > 20
)
.findFirst();
first.ifPresent(
System.out::println
);
The result is wrapped in Optional because the
stream may contain no matching element.
collect()
The collect() operation gathers stream results
into collections or other result containers.
List<Integer> evenNumbers =
numbers.stream()
.filter(
number -> number % 2 == 0
)
.collect(
Collectors.toList()
);
Collecting with toList()
List<String> result =
names.stream()
.filter(
name -> name.length() > 4
)
.collect(
Collectors.toList()
);
Collecting with toSet()
Set<String> uniqueNames =
names.stream()
.collect(
Collectors.toSet()
);
Collectors.joining()
joining() can combine strings into a single
string.
String result =
names.stream()
.collect(
Collectors.joining(", ")
);
System.out.println(result);
groupingBy()
groupingBy() groups elements according to a
classification function.
Map<Integer, List<String>> grouped =
names.stream()
.collect(
Collectors.groupingBy(
String::length
)
);
partitioningBy()
partitioningBy() divides elements into two
groups based on a boolean condition.
Map<Boolean, List<Integer>> partitioned =
numbers.stream()
.collect(
Collectors.partitioningBy(
number -> number % 2 == 0
)
);
peek()
peek() can be useful for observing elements while
debugging a stream pipeline. It should not normally be used
as the main mechanism for application side effects.
numbers.stream()
.filter(
number -> number > 10
)
.peek(
number ->
System.out.println(
"Before map: " + number
)
)
.map(
number -> number * 2
)
.forEach(
System.out::println
);
Lazy Evaluation
Intermediate operations are lazy. They are not generally executed until a terminal operation is invoked.
Stream<Integer> stream =
numbers.stream()
.filter(
number -> number > 10
);
System.out.println(
"Pipeline created"
);
stream.forEach(
System.out::println
);
Streams Cannot Normally Be Reused
A stream is intended for a single pipeline execution. After a terminal operation consumes it, attempting to reuse the same stream results in an exception.
Stream<String> stream =
names.stream();
stream.forEach(
System.out::println
);
// Do not reuse the same stream here.
Parallel Streams
A parallel stream can process elements using multiple threads. It can be useful for suitable CPU-intensive workloads, but it should not be assumed to be faster for every task.
numbers.parallelStream()
.filter(
number -> number > 10
)
.forEach(
System.out::println
);
Stream and Optional
Operations such as findFirst(),
findAny(), min(), and
max() commonly return Optional
because a result may not exist.
Optional<Integer> result =
numbers.stream()
.filter(
number -> number > 100
)
.findFirst();
result.ifPresent(
System.out::println
);
Primitive Streams
Java provides specialized streams for primitive numeric types:
IntStream, LongStream, and
DoubleStream.
IntStream.range(
1,
6
)
.forEach(
System.out::println
);
Numeric Stream Operations
IntSummaryStatistics statistics =
numbers.stream()
.mapToInt(
Integer::intValue
)
.summaryStatistics();
System.out.println(
statistics.getCount()
);
System.out.println(
statistics.getSum()
);
System.out.println(
statistics.getAverage()
);
System.out.println(
statistics.getMin()
);
System.out.println(
statistics.getMax()
);
Example 👨🏫🤓
Suppose an application needs to find active employees whose salary is above a particular threshold and return their names.
List<Employee> employees =
getEmployees();
List<String> result =
employees.stream()
.filter(
employee ->
employee.isActive()
)
.filter(
employee ->
employee.getSalary() > 50000
)
.map(
Employee::getName
)
.sorted()
.collect(
Collectors.toList()
);
Benefits of Stream API
- Provides concise data-processing pipelines.
- Works naturally with lambda expressions.
- Supports filtering, mapping, sorting, grouping, and reduction.
- Encourages declarative programming.
- Supports sequential and parallel processing models.
Stream API Best Practices
- Keep stream pipelines readable.
- Avoid unnecessary stream operations.
- Prefer method references when they improve readability.
- Avoid modifying shared mutable state inside stream operations.
-
Use
peek()mainly for debugging or observation. - Use parallel streams only after considering workload and thread-safety implications.
Interview Questions
filter() selects elements based on a
condition, while map() transforms
elements into another form.
reduce() combines stream elements into
a single result using an accumulator operation.
flatMap() transforms each element into
a stream and then flattens those streams into one
stream.
Summary
The Java Stream API provides a powerful way to process collections and other data sources using declarative pipelines. Important operations include filter, map, flatMap, distinct, sorted, limit, skip, reduce, collect, groupingBy, matching operations, and primitive streams.