In the previous article, we explored how to create streams in Java using:
- Collections
- Arrays
Stream.of()Stream.generate()
Now that we know how to create streams, it’s time to understand the real power of Java Streams — Intermediate Operations.
Intermediate operations allow us to transform, filter, and manipulate data inside a stream before producing the final result.
Let’s dive deep into intermediate operations in Java Streams with practical examples.
What are Intermediate Operations?
Intermediate operations are operations that:
- Transform a stream into another stream
- Do NOT produce final results
- Are lazy (they run only when a terminal operation is executed)
- Can be chained together
Example
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.forEach(System.out::println);
Here:
-
filter()→ intermediate -
map()→ intermediate -
forEach()→ terminal operation
Without a terminal operation, intermediate operations won’t execute.
Why Intermediate Operations Are Lazy?
Streams use lazy evaluation to improve performance.
This means:
- No processing happens until a terminal operation is called
- Operations are applied only when needed
- Helps optimize performance
Example
Stream<Integer> stream = Stream.of(1,2,3,4,5)
.filter(n -> {
System.out.println("Filtering: " + n);
return n % 2 == 0;
});
System.out.println("No terminal operation yet!");
Nothing prints because no terminal operation is present.
Add terminal operation:
stream.forEach(System.out::println);
Now filtering happens.
Common Intermediate Operations in Java Streams
Let’s explore the most important ones.
1. filter()
Used to filter elements based on condition.
Syntax
stream.filter(condition)
Example
List<String> names = List.of("Ram", "Shyam", "Amit", "Ankit");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
Output
Amit
Ankit
2. map()
Transforms each element into another form.
Syntax
stream.map(function)
Example
List<Integer> numbers = List.of(1,2,3,4);
numbers.stream()
.map(n -> n * n)
.forEach(System.out::println);
Output
1
4
9
16
Used for:
- Transforming data
- Converting objects
- Extracting fields
3. flatMap()
Used to flatten nested structures.
Example: List of lists
List<List<Integer>> list = List.of(
List.of(1,2),
List.of(3,4),
List.of(5,6)
);
list.stream()
.flatMap(l -> l.stream())
.forEach(System.out::println);
Output
1 2 3 4 5 6
flatMap() converts Stream → Stream
4. distinct()
Removes duplicate elements.
Example
List<Integer> numbers = List.of(1,2,2,3,4,4,5);
numbers.stream()
.distinct()
.forEach(System.out::println);
Output
1 2 3 4 5
5. sorted()
Sorts stream elements.
Natural sorting
List<Integer> list = List.of(5,1,4,2,3);
list.stream()
.sorted()
.forEach(System.out::println);
Output
1
2
3
4
5
Custom sorting
List<String> names = List.of("Ram","Amit","Shyam");
names.stream()
.sorted((a,b) -> b.compareTo(a))
.forEach(System.out::println);
Output
Amit
Ram
Shyam
Note: The sorted() function first sorts the entire list and only after it finishes its operation, forEach() prints the list.
6. limit()
Limits number of elements.
Stream.iterate(1, n -> n + 1)
.limit(5)
.forEach(System.out::println);
Output
1
2
3
4
5
7. skip()
Skips first N elements.
List<Integer> list = List.of(1,2,3,4,5,6);
list.stream()
.skip(3)
.forEach(System.out::println);
Output
4
5
6
8. peek()
Used for debugging (not recommended for logic).
List<Integer> list = List.of(1,2,3);
list.stream()
.peek(n -> System.out.println("Processing: " + n))
.map(n -> n * 2)
.forEach(System.out::println);
Output
Processing: 1
2
Processing: 2
4
Processing: 3
6
Used mainly for:
- Debugging
- Logging stream flow
Chaining Multiple Intermediate Operations
This is where streams make coding simpler and easier to comprehend.
List<String> names = List.of("Ram", "Amit", "Shyam", "Ankit", "Rahul");
names.stream()
.filter(n -> n.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
Output
AMIT
ANKIT
Key Characteristics of Intermediate Operations
| Feature | Description |
|---|---|
| Lazy | Executes only after terminal operation |
| Returns Stream | Always returns another stream |
| Chainable | Multiple operations can be chained |
| Non-destructive | Original data remains unchanged |
Real-World Example (Employee Processing)
Problem statement: Given a list of employees, print the names of all the employees with salary greater than 50000 in descending order of their salaries i.e employee with highest salary should be printed first.
class Employee {
String name;
int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
public String toString() {
return name + " : " + salary;
}
}
public class Main {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Ram", 50000),
new Employee("Amit", 70000),
new Employee("Shyam", 40000),
new Employee("Ankit", 90000)
);
employees.stream()
.filter(e -> e.salary > 50000)
.sorted((a,b) -> b.salary - a.salary)
.map(e -> e.name)
.forEach(System.out::println);
}
}
Output
Ankit
Amit
Conclusion
Intermediate operations are the backbone of Java Streams.
They help you:
- Write clean functional code
- Process data efficiently
- Avoid complex loops
- Improve readability and performance
Once you master intermediate operations like filter(), map(), flatMap(), and sorted(), you unlock the true power of Java Streams.
What’s Next?
In the next article, we’ll cover:
Terminal Operations in Java Streams
forEach()collect()reduce()count()findFirst()anyMatch()
This is Part 4 of the Java Streams Series.
If you find it insightful, please share your feedback. Also let me know if you have used builder pattern in your projects.
Next Up: Terminal Operations!
Top comments (0)