Traditional Java often uses loops, if statements, temporary collections, and manual sorting. This works, but the code can become long as the processing logic grows.
Java Streams were introduced in Java 8 to make data processing more declarative. Instead of explaining every step of the loop, you describe the result you want.
Imperative style:
How should the data be processed?
Declarative style:
What result do I want?
A Java Stream is a pipeline that processes data from a source through lazy intermediate operations and finishes with a terminal operation.
Suppose we have employees and want the names of employees earning more than 50,000, sorted alphabetically.
List<String> result = new ArrayList<>();
for (Employee employee : employees) {
if (employee.getSalary() > 50_000) {
result.add(employee.getName());
}
}
Collections.sort(result);This code explains every step:
- Create a result list.
- Visit each employee.
- Check the salary.
- Add the name if the condition passes.
- Sort the result.
List<String> result =
employees.stream()
.filter(employee -> employee.getSalary() > 50_000)
.map(Employee::getName)
.sorted()
.toList();This describes the desired result:
employees
→ keep employees with salary > 50,000
→ convert employees to names
→ sort names
→ collect them into a list
The PDF presents this same transformation from manual loops to a stream pipeline.
A Stream is a tool for processing a sequence of data through a chain of operations.
The data can come from:
- a
List, - a
Set, - an array,
- individual values,
- an infinite generator,
- a primitive stream such as
IntStream.
A Stream does not replace the original collection. It processes data from that source.
This is one of the most important points.
A List stores data:
List<Integer> numbers =
List.of(7, 11, 23, 43, 71, 10);A Stream does not permanently store a second copy of that data:
Stream<Integer> stream = numbers.stream();The stream is a processing view over numbers.
Collection
↓
Stream view
↓
Process elements
↓
Result
The stream itself is not a replacement for List, Set, or another data structure.
The PDF compares a Java Stream to a water pipeline.
Source → Filter 1 → Filter 2 → Transformation → Destination
For data:
List
↓
filter
↓
map
↓
sorted
↓
toList
Data enters at the source, passes through operations, and finally reaches a result.
Every stream pipeline has three parts:
Source
↓
Intermediate operations
↓
Terminal operation
The source provides the original data.
Examples:
numbers.stream();
Arrays.stream(array);
Stream.of(1, 2, 3);Intermediate operations modify or describe the processing pipeline.
Examples:
filter()
map()
sorted()
distinct()
limit()
skip()They are lazy and return another Stream.
Stream<Integer> result =
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2);At this point, the stream has not processed the elements yet.
A terminal operation completes the pipeline and starts execution.
Examples:
forEach()
toList()
collect()
count()
findFirst()Example:
numbers.stream()
.filter(x -> x > 10)
.forEach(System.out::println);forEach() is the terminal operation.
List<Integer> numbers =
List.of(7, 11, 23, 43, 71, 10);
List<Integer> result =
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.toList();
System.out.println(result);Step by step:
Original:
7, 11, 23, 43, 71, 10
After filter x > 10:
11, 23, 43, 71
After map x * 2:
22, 46, 86, 142
Final list:
22, 46, 86, 142
filter() keeps elements that satisfy a condition.
List<Integer> result =
numbers.stream()
.filter(x -> x > 10)
.toList();Input:
7, 11, 23, 43, 71, 10
Output:
11, 23, 43, 71
The lambda passed to filter() is a predicate:
x -> x > 10It returns true or false.
map() transforms every element.
List<Integer> squares =
numbers.stream()
.map(x -> x * x)
.toList();Input:
1, 2, 3, 4
Output:
1, 4, 9, 16
Example with objects:
List<String> names =
employees.stream()
.map(Employee::getName)
.toList();Each Employee becomes a String.
sorted() orders the elements.
List<Integer> sorted =
numbers.stream()
.sorted()
.toList();Example:
Input: 7, 2, 9, 1
Output: 1, 2, 7, 9
Custom sorting can use a comparator:
students.stream()
.sorted(Comparator.comparing(Student::getMarks))
.toList();distinct() removes duplicate elements.
List<Integer> result =
List.of(1, 2, 2, 3, 3, 3)
.stream()
.distinct()
.toList();Output:
1, 2, 3
limit() keeps only a fixed number of elements.
List<Integer> firstThree =
numbers.stream()
.limit(3)
.toList();If the source is:
10, 20, 30, 40, 50
The result is:
10, 20, 30
skip() ignores the first elements.
List<Integer> remaining =
numbers.stream()
.skip(2)
.toList();For:
10, 20, 30, 40
The result is:
30, 40
forEach() performs an action on every element.
numbers.stream()
.forEach(System.out::println);The method reference is equivalent to:
numbers.stream()
.forEach(x -> System.out.println(x));toList() collects the processed elements into a list.
List<Integer> result =
numbers.stream()
.filter(x -> x % 2 == 0)
.toList();collect() is a flexible terminal operation used with collectors.
List<String> names =
employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());Modern Java also supports:
List<String> names =
employees.stream()
.map(Employee::getName)
.toList();count() returns the number of elements.
long count =
numbers.stream()
.filter(x -> x > 10)
.count();findFirst() returns the first matching element.
Optional<Integer> first =
numbers.stream()
.filter(x -> x > 20)
.findFirst();The result is an Optional because there may be no matching element.
Streams use lazy evaluation. Intermediate operations do not execute immediately.
Example:
Stream<Integer> stream =
numbers.stream()
.filter(x -> {
System.out.println("Filtering " + x);
return x > 10;
});Nothing is printed yet because there is no terminal operation.
Execution starts here:
stream.forEach(System.out::println);The terminal operation triggers the pipeline.
Lazy evaluation allows Java to:
- avoid unnecessary work,
- combine operations,
- stop early,
- process values only when needed.
Build pipeline
↓
No processing yet
↓
Terminal operation
↓
Pipeline executes
This code creates a pipeline but does not run it:
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2);The result is ignored, and no terminal operation exists.
To execute it:
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.forEach(System.out::println);The PDF emphasizes this rule: without a terminal operation, the stream is not executed.
Some terminal operations stop as soon as they have enough information. This is called short-circuiting.
findFirst() is an example.
Optional<Integer> result =
numbers.stream()
.filter(x -> x > 20)
.findFirst();If the first matching number is found, the stream does not need to process the remaining values.
This behaves somewhat like break in a loop.
List<Integer> numbers =
List.of(5, 12, 7, 2, 40, 60);
Optional<Integer> result =
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.findFirst();Processing may look like:
5 → rejected
12 → accepted → mapped to 24 → stop
The values 7, 2, 40, and 60 may never be processed.
A stream pipeline does not always process the entire list through the first operation, then the entire list through the second operation. It can process one element through the whole pipeline before moving to the next.
Example:
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.forEach(System.out::println);Conceptually:
Element 1 → filter → map → output
Element 2 → filter → map → output
Element 3 → filter → map → output
This vertical flow is especially useful when short-circuiting can stop the pipeline early.
After a terminal operation, the stream is closed or “dead.”
This is invalid:
Stream<Integer> stream =
numbers.stream();
stream.count();
stream.forEach(System.out::println); // errorA stream must be recreated:
numbers.stream().count();
numbers.stream()
.forEach(System.out::println);The collection can be reused, but the stream object cannot be reused after a terminal operation.
Collection → can usually create many streams
Stream → one-use processing pipeline
List<Integer> numbers =
List.of(1, 2, 3, 4);
Stream<Integer> stream =
numbers.stream();The PDF identifies collections as one of the most common stream sources.
Stream<Integer> stream =
numbers.parallelStream();This creates a parallel stream.
A parallel stream may process elements using multiple threads, but it should not be used automatically. The work must be suitable for parallel execution, and the overhead may not be worth it for small collections.
int[] numbers = {1, 2, 3, 4};
IntStream stream =
Arrays.stream(numbers);For an object array:
String[] names = {"Ravi", "Anita"};
Stream<String> stream =
Arrays.stream(names);Stream<Integer> stream =
Stream.of(1, 2, 3, 4);Example:
Stream<String> names =
Stream.of("Ravi", "Anita", "Karan");The PDF includes Stream.empty() as a source.
Stream<String> empty =
Stream.empty();This is useful when a method must return a stream but has no values.
The PDF introduces two ways to create infinite streams:
Stream.iterate()
Stream.generate()
These must usually be combined with limit().
iterate() creates values based on the previous value.
Stream.iterate(1, x -> x + 1)
.limit(10)
.forEach(System.out::println);Output:
1
2
3
4
5
6
7
8
9
10
The flow is:
Start with 1
→ add 1
→ add 1
→ add 1
→ continue
Another example:
List<Integer> powers =
Stream.iterate(1, x -> x * 2)
.limit(6)
.toList();Output:
1, 2, 4, 8, 16, 32
Each value depends on the previous value.
generate() creates values from a supplier. The next value does not depend on the previous value.
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);The values are generated independently.
Another example:
List<String> values =
Stream.generate(() -> "Java")
.limit(3)
.toList();Output:
Java, Java, Java
This can run forever:
Stream.iterate(1, x -> x + 1)
.forEach(System.out::println);There is no natural endpoint.
Use limit():
Stream.iterate(1, x -> x + 1)
.limit(10)
.forEach(System.out::println);limit() gives the infinite source a finite stopping point.
Normal object streams use wrapper types:
Stream<Integer>
Stream<Long>
Stream<Double>This may involve boxing and unboxing.
Java provides primitive streams to avoid that overhead:
IntStream
LongStream
DoubleStreamThe PDF specifically introduces primitive streams for performance and shows conversions between object streams and primitive streams.
IntStream numbers =
IntStream.of(1, 2, 3, 4);Example:
int sum =
IntStream.of(1, 2, 3, 4)
.sum();
System.out.println(sum); // 10There is no need to use Integer objects for each primitive value.
long total =
LongStream.of(10L, 20L, 30L)
.sum();
System.out.println(total); // 60double average =
DoubleStream.of(10.5, 20.5, 30.5)
.average()
.orElse(0.0);
System.out.println(average); // 20.5Use mapToInt() when converting an object stream to an IntStream.
List<String> names =
List.of("Java", "Stream", "API");
int totalLength =
names.stream()
.mapToInt(String::length)
.sum();
System.out.println(totalLength); // 14The type transformation is:
Stream<String>
↓ mapToInt()
IntStream
This is often useful when extracting numeric data from objects.
double totalSalary =
employees.stream()
.mapToDouble(Employee::getSalary)
.sum();Use boxed() to convert a primitive stream back into an object stream.
List<Integer> numbers =
IntStream.of(1, 2, 3)
.boxed()
.toList();The type transformation is:
IntStream
↓ boxed()
Stream<Integer>
Use this when an API requires wrapper objects or a collection.
The PDF also includes:
mapToLong()
mapToDouble()Examples:
LongStream salaries =
employees.stream()
.mapToLong(Employee::getSalary);DoubleStream prices =
products.stream()
.mapToDouble(Product::getPrice);import java.util.List;
class Employee {
private final String name;
private final int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
String getName() {
return name;
}
int getSalary() {
return salary;
}
}
public class Main {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Ravi", 45_000),
new Employee("Anita", 70_000),
new Employee("Karan", 60_000),
new Employee("Meera", 80_000)
);
List<String> result =
employees.stream()
.filter(employee -> employee.getSalary() > 50_000)
.map(Employee::getName)
.sorted()
.toList();
System.out.println(result);
}
}Processing:
Employees:
Ravi 45,000
Anita 70,000
Karan 60,000
Meera 80,000
After filter:
Anita
Karan
Meera
After map:
"Anita"
"Karan"
"Meera"
After sorted:
"Anita"
"Karan"
"Meera"
- A Stream is not a collection and does not store data independently.
- Intermediate operations are lazy.
- Without a terminal operation, the pipeline does not execute.
- A stream cannot be reused after a terminal operation.
- Infinite streams require a stopping operation such as
limit(). findFirst()may stop processing early.map()transforms values;filter()removes values.boxed()changes a primitive stream into an object stream.mapToInt()changes an object stream into anIntStream.- Primitive streams help avoid boxing and unboxing overhead.
- Do not assume
parallelStream()is always faster. - Avoid modifying the source collection while processing its stream.
| Feature | Collection | Stream |
|---|---|---|
| Stores data | Yes | No |
| Main purpose | Hold data | Process data |
| Reusable | Usually yes | No, after terminal operation |
| Processing style | Manual loops or APIs | Pipeline operations |
| Execution | Immediate when code runs | Lazy until terminal operation |
| Example | List<Integer> |
numbers.stream() |
- Streams process sequences of data through pipelines.
- A stream has a source, intermediate operations, and a terminal operation.
- Intermediate operations are lazy.
- A terminal operation triggers execution.
- Streams do not store data.
- Streams cannot be reused after terminal operations.
- Short-circuiting operations can stop processing early.
- Collections, arrays,
Stream.of(), empty streams, and infinite generators can all be sources. iterate()depends on the previous value.generate()creates values independently.IntStream,LongStream, andDoubleStreamreduce boxing and unboxing.mapToInt(),mapToLong(), andmapToDouble()convert object streams to primitive streams.boxed()converts primitive streams back to object streams.- Streams support a more declarative style of programming.
- What are the three parts of a stream pipeline?
- Why does this code not print anything?
numbers.stream()
.filter(x -> x > 10)
.map(x -> x * 2);- What starts stream execution?
- Why can a stream not be reused after
count()? - What is the difference between
map()andfilter()? - What is the difference between
Stream.iterate()andStream.generate()? - Why is
limit()needed for many infinite streams? - What is the purpose of
IntStream? - When would you use
boxed()? - What happens in this pipeline?
List.of(5, 12, 7, 2, 40)
.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.findFirst();- Rewrite a loop that selects even numbers as a stream pipeline.
- Explain the difference between imperative and declarative programming.
The most logical next topics are:
map,filter, andreducein depth.collect()and collectors such asgroupingBy().Optionaland terminal operations likefindFirst().- Stream ordering and parallel streams.
- Stateful operations such as
sorted()anddistinct(). - Stream performance, short-circuiting, and lazy evaluation.