A Stream pipeline is built from:
Source
↓
Intermediate operations
↓
Terminal operation
Example:
List<Integer> result =
List.of(3, 5, 10, 13, 17, 19)
.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.toList();Here:
List.of(...)is the source.filter()andmap()are intermediate operations.toList()is the terminal operation.
The PDF groups stream methods into intermediate operations, terminal operations, and collectors.
Intermediate operations build a lazy processing pipeline, while terminal operations start that pipeline and produce a result or side effect.
Intermediate operations:
- return another stream,
- can be chained,
- are lazy,
- do not execute until a terminal operation is called.
stream()
.filter(...)
.map(...)
.sorted()
.toList()
In this example, filter(), map(), and sorted() are intermediate operations. toList() is terminal.
filter() keeps only the elements that satisfy a condition.
List<Integer> result =
List.of(3, 5, 10, 13, 17, 19)
.stream()
.filter(x -> x > 10)
.toList();
System.out.println(result);Output:
13, 17, 19
The lambda returns a boolean:
x -> x > 10Meaning:
Keep x only when x is greater than 10.
map() transforms every element into another value.
List<Integer> result =
List.of(1, 2, 3, 4)
.stream()
.map(x -> x * x)
.toList();
System.out.println(result);Output:
1, 4, 9, 16
map() keeps one output for each input.
Input: 1, 2, 3, 4
Output: 1, 4, 9, 16
It can also change the type:
List<String> names =
students.stream()
.map(Student::getName)
.toList();Student → String
flatMap() is used when each element contains multiple elements, such as a List<List<Integer>>. It flattens the nested structure into one stream.
Without flatMap():
List<List<Integer>> numbers =
List.of(
List.of(1, 2),
List.of(3, 4),
List.of(5, 6)
);This is a list of lists:
[ [1, 2], [3, 4], [5, 6] ]
Using flatMap():
List<Integer> result =
numbers.stream()
.flatMap(list -> list.stream())
.toList();
System.out.println(result);Output:
1, 2, 3, 4, 5, 6
The transformation is:
List<List<Integer>>
↓ flatMap()
Stream<Integer>
↓
List<Integer>
map() preserves the nested structure:
numbers.stream()
.map(list -> list.stream());Conceptually:
List<Integer> → Stream<Integer>
The result becomes a stream of streams.
flatMap() removes one level of nesting:
numbers.stream()
.flatMap(list -> list.stream());Conceptually:
List<List<Integer>> → Stream<Integer>
Use flatMap() when one input can produce multiple output elements.
sorted() arranges stream elements in order.
List<Integer> result =
List.of(5, 1, 4, 2, 3)
.stream()
.sorted()
.toList();
System.out.println(result);Output:
1, 2, 3, 4, 5
For descending order:
List<Integer> result =
List.of(5, 1, 4, 2, 3)
.stream()
.sorted(Comparator.reverseOrder())
.toList();sorted() is a stateful intermediate operation. It must see all or many elements before it knows the correct order.
For example, after seeing only 5, Java cannot know whether a smaller value will appear later.
5
5, 1
5, 1, 4
5, 1, 4, 2
...
The stream must collect enough information, sort it, and then pass elements forward.
distinct() removes duplicate elements.
List<Integer> result =
List.of(1, 2, 2, 3, 3, 3)
.stream()
.distinct()
.toList();
System.out.println(result);Output:
1, 2, 3
The PDF notes that distinct() uses hashing to identify repeated values.
distinct() must remember values it has already seen.
Input:
1, 2, 2, 3, 1
Seen values:
1 → keep
2 → keep
2 → remove
3 → keep
1 → remove
It is stateful because it needs memory of previous elements.
For custom objects, correct equals() and hashCode() implementations are important:
class Student {
String name;
int rollNo;
@Override
public boolean equals(Object obj) {
// logical equality
return true;
}
@Override
public int hashCode() {
// consistent hash code
return 1;
}
}limit(n) keeps at most the first n elements.
List<Integer> result =
List.of(1, 2, 3, 4, 5)
.stream()
.limit(3)
.toList();
System.out.println(result);Output:
1, 2, 3
limit() is especially useful with infinite streams:
List<Integer> numbers =
Stream.iterate(1, x -> x + 1)
.limit(5)
.toList();
System.out.println(numbers);Output:
1, 2, 3, 4, 5
Without limit(), the infinite stream would continue forever.
skip(n) discards the first n elements.
List<Integer> result =
List.of(1, 2, 3, 4, 5)
.stream()
.skip(2)
.toList();
System.out.println(result);Output:
3, 4, 5
skip() and limit() can be combined for pagination.
int pageSize = 3;
int pageNumber = 2;
List<Integer> page =
numbers.stream()
.skip((long) (pageNumber - 1) * pageSize)
.limit(pageSize)
.toList();For page 2, Java skips the first page and takes the next three elements.
peek() lets you observe elements as they pass through the pipeline. It is mainly useful for debugging.
List<Integer> result =
List.of(1, 2, 3, 4)
.stream()
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("After filter: " + x))
.map(x -> x * 10)
.peek(x -> System.out.println("After map: " + x))
.toList();Possible output:
After filter: 2
After map: 20
After filter: 4
After map: 40
Important: peek() is lazy too. Without a terminal operation, it does nothing:
numbers.stream()
.peek(System.out::println);Nothing is printed.
This is risky:
numbers.stream()
.peek(x -> saveToDatabase(x))
.toList();If the pipeline changes or the terminal operation is removed, the save operation may not run. Use forEach() or an explicit method when the side effect is important.
The PDF lists mapToInt() and mapToDouble() as stream operations.
They convert object streams into primitive streams.
List<String> names =
List.of("Java", "Stream", "API");
int totalLength =
names.stream()
.mapToInt(String::length)
.sum();
System.out.println(totalLength);Output:
13
The transformation is:
Stream<String>
↓ mapToInt()
IntStream
Primitive streams avoid unnecessary boxing and unboxing.
Terminal operations:
- trigger stream execution,
- end the pipeline,
- produce a result or side effect,
- make the stream unusable afterward.
The PDF groups terminal operations into:
- collecting results,
- reducing values,
- searching and matching,
- iteration,
- numeric operations.
forEach() performs an action for every element.
List.of(1, 2, 3, 4)
.stream()
.forEach(System.out::println);Output:
1
2
3
4
It returns void.
forEachOrdered() preserves encounter order, especially when working with parallel streams.
numbers.parallelStream()
.forEachOrdered(System.out::println);For a sequential stream, forEach() normally follows the source order. For a parallel stream, forEach() may process elements in an unpredictable order.
numbers.parallelStream()
.forEach(System.out::println);Possible output:
3
1
4
2
Use forEachOrdered() when output order matters.
collect() gathers stream elements into a result. It usually works with the Collectors utility class.
List<Integer> list =
numbers.stream()
.filter(x -> x > 5)
.collect(Collectors.toList());Modern Java also supports:
List<Integer> list =
numbers.stream()
.filter(x -> x > 5)
.toList();Set<Integer> set =
numbers.stream()
.collect(Collectors.toSet());A set removes duplicates.
Map<Integer, String> map =
students.stream()
.collect(Collectors.toMap(
Student::getRollNo,
Student::getName
));The result may look like:
1 → Anita
2 → Ravi
3 → Karan
The key must be unique unless you provide a merge function.
String result =
names.stream()
.collect(Collectors.joining(", "));Output:
Ravi, Anita, Karan
reduce() combines stream elements into one final value.
For example, summing numbers:
int sum =
List.of(1, 2, 3, 4)
.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 10The reduction happens like this:
Initial value: 0
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
The general form is:
reduce(identity, accumulator)identityis the starting value.accumulatorexplains how to combine values.
Another example:
Optional<Integer> maximum =
numbers.stream()
.reduce(Integer::max);Without an identity value, the result is an Optional because the stream might be empty.
Use reduce() when many values become one value:
Numbers → one sum
Strings → one combined result
Use collect() when you want to build a collection or structured result:
Stream → List
Stream → Set
Stream → Map
count() returns the number of stream elements as a long.
long count =
List.of(1, 2, 3, 4, 5)
.stream()
.filter(x -> x % 2 == 0)
.count();
System.out.println(count); // 2The return type is long, not int.
long total = numbers.stream().count();Short-circuiting operations stop as soon as the final answer is known.
The PDF lists:
findFirst()
findAny()
anyMatch()
allMatch()
noneMatch()
findFirst() returns the first matching element.
Optional<Integer> result =
List.of(3, 5, 10, 13, 17)
.stream()
.filter(x -> x > 10)
.findFirst();
System.out.println(result.orElse(-1)); // 13Processing:
3 → reject
5 → reject
10 → reject
13 → accept and stop
The remaining values may not be processed.
findAny() returns any matching element.
Optional<Integer> result =
numbers.stream()
.filter(x -> x > 10)
.findAny();With a sequential stream, it often returns the first matching element, but that is not the guarantee. With parallel streams, it may return whichever matching element is found first.
Use:
findFirst()when order matters.findAny()when any match is acceptable, especially in parallel processing.
anyMatch() returns true if at least one element matches.
boolean hasEven =
List.of(1, 3, 5, 8, 9)
.stream()
.anyMatch(x -> x % 2 == 0);
System.out.println(hasEven); // trueOnce 8 is found, the stream can stop.
allMatch() returns true only if every element matches.
boolean allPositive =
List.of(2, 4, 6, 8)
.stream()
.allMatch(x -> x > 0);
System.out.println(allPositive); // trueIf one value fails, processing can stop:
boolean allPositive =
List.of(2, 4, -6, 8)
.stream()
.allMatch(x -> x > 0);
System.out.println(allPositive); // falsenoneMatch() returns true if no element matches.
boolean noNegative =
List.of(2, 4, 6)
.stream()
.noneMatch(x -> x < 0);
System.out.println(noNegative); // trueIf a negative value is found, the answer becomes false.
Primitive streams provide numeric operations such as:
sum()
max()
min()
average()
count()
These are especially useful with IntStream, LongStream, and DoubleStream.
int total =
IntStream.of(1, 2, 3, 4)
.sum();
System.out.println(total); // 10OptionalInt maximum =
IntStream.of(4, 8, 2, 10)
.max();
System.out.println(maximum.orElse(0)); // 10OptionalInt minimum =
IntStream.of(4, 8, 2, 10)
.min();
System.out.println(minimum.orElse(0)); // 2OptionalDouble average =
IntStream.of(10, 20, 30)
.average();
System.out.println(average.orElse(0.0)); // 20.0The result is optional because an empty stream has no maximum, minimum, or average.
groupingBy() organizes elements into a Map using a classifier function.
Suppose we have:
class Employee {
String name;
String department;
String getDepartment() {
return department;
}
}Group employees by department:
Map<String, List<Employee>> employeesByDepartment =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment
));The result might look like:
IT → [Ravi, Anita]
Finance → [Karan]
HR → [Meera, John]
Map<Integer, List<String>> grouped =
names.stream()
.collect(Collectors.groupingBy(
String::length
));For:
Java, Code, Stream, API
The result is conceptually:
3 → [API]
4 → [Java, Code]
6 → [Stream]
You can use another collector inside groupingBy():
Map<String, Long> countByDepartment =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));Result:
IT → 2
Finance → 1
HR → 2
partitioningBy() creates exactly two groups:
true
false
It is useful when every element either satisfies a condition or does not.
Example:
Map<Boolean, List<Integer>> partitioned =
numbers.stream()
.collect(Collectors.partitioningBy(
x -> x % 2 == 0
));Result:
true → [2, 4, 6]
false → [1, 3, 5]
| Method | Number of groups | Use |
|---|---|---|
groupingBy() |
Any number | Group by a category or key |
partitioningBy() |
Exactly two | Divide by a true/false condition |
Example:
groupingBy(Employee::getDepartment)creates groups such as IT, HR, and Finance.
partitioningBy(Employee::isActive)creates only:
true
false
joining() combines string elements into one string with an optional delimiter.
String result =
names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);Output:
Ravi, Anita, Karan
String result =
names.stream()
.collect(Collectors.joining(
", ",
"[",
"]"
));
System.out.println(result);Output:
[Ravi, Anita, Karan]
The arguments are:
delimiter, prefix, suffix
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
class Student {
private final String name;
private final String department;
private final int marks;
Student(String name, String department, int marks) {
this.name = name;
this.department = department;
this.marks = marks;
}
String getName() {
return name;
}
String getDepartment() {
return department;
}
int getMarks() {
return marks;
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Ravi", "IT", 85),
new Student("Anita", "IT", 92),
new Student("Karan", "HR", 65),
new Student("Meera", "HR", 78)
);
List<String> topStudents =
students.stream()
.filter(student -> student.getMarks() >= 75)
.sorted((s1, s2) ->
Integer.compare(
s2.getMarks(),
s1.getMarks()
)
)
.map(Student::getName)
.toList();
Map<String, List<Student>> byDepartment =
students.stream()
.collect(Collectors.groupingBy(
Student::getDepartment
));
Map<Boolean, List<Student>> partitioned =
students.stream()
.collect(Collectors.partitioningBy(
student -> student.getMarks() >= 75
));
String names =
students.stream()
.map(Student::getName)
.collect(Collectors.joining(", "));
int totalMarks =
students.stream()
.mapToInt(Student::getMarks)
.sum();
System.out.println(topStudents);
System.out.println(byDepartment);
System.out.println(partitioned);
System.out.println(names);
System.out.println(totalMarks);
}
}| Category | Operations | Behavior |
|---|---|---|
| Intermediate | filter, map, flatMap |
Transform or select elements |
| Intermediate | sorted, distinct |
Stateful processing |
| Intermediate | limit, skip |
Slice the stream |
| Intermediate | peek |
Observe elements |
| Intermediate | mapToInt, mapToDouble |
Convert to primitive streams |
| Terminal | forEach, forEachOrdered |
Perform an action |
| Terminal | toList, collect |
Build a result |
| Terminal | reduce |
Combine values |
| Terminal | count |
Count elements |
| Terminal | findFirst, findAny |
Find an element |
| Terminal | anyMatch, allMatch, noneMatch |
Test conditions |
| Primitive terminal | sum, min, max, average |
Calculate statistics |
- Intermediate operations are lazy.
- A terminal operation is needed to execute the pipeline.
sorted()anddistinct()are stateful because they need information about multiple elements.flatMap()removes one level of nesting, whilemap()keeps the structure.peek()is mainly for debugging.forEach()may not preserve order in a parallel stream.- Use
forEachOrdered()when encounter order matters. findFirst()respects order;findAny()does not promise a specific matching element.count()returnslong.max(),min(), andaverage()on primitive streams return optional numeric types because the stream may be empty.groupingBy()can create many groups.partitioningBy()creates onlytrueandfalsegroups.reduce()combines values into one result.collect()usually creates a collection or structured result.- A stream cannot be reused after a terminal operation.
- Do not use stream side effects carelessly.
- Intermediate operations return streams and are lazy.
- Terminal operations start execution and finish the pipeline.
filter()selects elements.map()transforms one element into one result.flatMap()flattens nested data.sorted()orders elements but must observe the stream state.distinct()removes duplicates using equality and hashing.limit()takes the first part.skip()ignores the first part.peek()helps inspect a pipeline during debugging.forEach()performs an action.collect()builds lists, sets, maps, or strings.reduce()combines elements into one value.- Matching and finding operations can stop early.
- Primitive streams provide numeric operations efficiently.
groupingBy()creates category-based groups.partitioningBy()separates values into two groups.joining()combines strings.
- Which operations in this pipeline are intermediate and which one is terminal?
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.sorted()
.toList();- What is the difference between
map()andflatMap()? - Why is
sorted()stateful? - Why is
distinct()stateful? - Why is
peek()usually used for debugging? - What is the difference between
forEach()andforEachOrdered()? - What does
reduce()do? - Why does
count()returnlong? - When should you use
groupingBy()? - When should you use
partitioningBy()? - What output does this produce?
List.of(1, 2, 2, 3, 4)
.stream()
.distinct()
.filter(x -> x % 2 == 0)
.map(x -> x * 10)
.toList();-
Write a stream pipeline that:
- removes duplicate numbers,
- keeps only even numbers,
- squares them,
- collects them into a list.
-
Write a stream pipeline that groups employees by department.
-
Write a stream pipeline that joins names using
", ".
The next useful topics are:
Collectors.toMap()and duplicate-key handling.groupingBy()with downstream collectors such ascounting(),mapping(), andsummarizingInt().reduce()versuscollect().- Stream ordering and parallel streams.
OptionalwithfindFirst(),min(), andmax().- Stream performance and short-circuiting.