When we work with collections such as ArrayList, we often need to perform operations on their elements.
For example, suppose we have a list of student marks:
ArrayList<Integer> marks = new ArrayList<>();
marks.add(85);
marks.add(42);
marks.add(91);
marks.add(67);
marks.add(30);Now imagine we want to find only the marks greater than 60.
We could use a normal loop:
for (int mark : marks) {
if (mark > 60) {
System.out.println(mark);
}
}This works perfectly fine. But Java provides another way to process collections using the Stream API.
The Stream API allows us to process data from collections in a clean and readable way using operations such as filtering, sorting, and transforming.
What Is a Stream?
A stream is a sequence of elements that we can process step by step.
For example:
marks.stream()creates a stream from the marks collection.
We can then perform operations on that stream.
marks.stream()
.filter(mark -> mark > 60)
.forEach(mark -> System.out.println(mark));Output:
85
91
67This reads almost like English:
Take the marks, keep the marks greater than 60, and print each one.
Creating a Stream
The most common way to create a stream is from a collection.
ArrayList<String> names = new ArrayList<>();
names.add("John");
names.add("Jason");
names.add("Alex");
names.stream();We can then process the stream.
For example:
names.stream()
.forEach(name -> System.out.println(name));Output:
John
Jason
Alexfilter()
The filter() method is used when we want to keep only elements that satisfy a condition.
For example:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(25);
numbers.add(30);
numbers.add(45);
numbers.add(50);We can find numbers greater than 30:
numbers.stream()
.filter(number -> number > 30)
.forEach(number -> System.out.println(number));Output:
45
50The lambda:
number -> number > 30acts as the condition.
map()
The map() method is used when we want to transform each element into something else.
For example, suppose we have:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);We can create a stream where every number is doubled:
numbers.stream()
.map(number -> number * 2)
.forEach(number -> System.out.println(number));Output:
2
4
6
8The original list isn't changed. The stream produces transformed values.
sorted()
We can use sorted() to sort elements.
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(50);
numbers.add(10);
numbers.add(40);
numbers.add(20);Now:
numbers.stream()
.sorted()
.forEach(number -> System.out.println(number));Output:
10
20
40
50This is useful when we want to process data in sorted order.
forEach()
forEach() is commonly used as the final operation when we simply want to perform an action for every element.
For example:
names.stream()
.forEach(name -> System.out.println("Hello, " + name));Output:
Hello, John
Hello, Jason
Hello, AlexHere, the lambda determines what should happen to each element.
collect()
Often, we don't just want to print the results. We want to store the processed elements in a new collection.
For this, we can use collect().
For example:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(25);
numbers.add(40);
numbers.add(55);
List<Integer> result = numbers.stream()
.filter(number -> number > 30)
.collect(Collectors.toList());
System.out.println(result);
}
}Output:
[40, 55]Here, we filtered the numbers and stored the result in a new List.
Chaining Stream Operations
One of the most useful features of streams is that we can chain multiple operations together.
For example:
numbers.stream()
.filter(number -> number > 20)
.map(number -> number * 2)
.sorted()
.forEach(number -> System.out.println(number));The data moves through each operation:
numbers
↓
filter()
↓
map()
↓
sorted()
↓
forEach()For example, if the original values are:
10, 25, 40, 15filter() keeps:
25, 40map() doubles them:
50, 80sorted() sorts them:
50, 80and forEach() prints them.
count()
We can use count() to find how many elements match a condition.
long count = numbers.stream()
.filter(number -> number > 30)
.count();
System.out.println(count);If the list contains:
10, 25, 40, 50the output will be:
2because 40 and 50 are greater than 30.
findFirst()
Sometimes we only want the first matching element.
For example:
Optional<Integer> result = numbers.stream()
.filter(number -> number > 30)
.findFirst();findFirst() returns an Optional because there might not be any matching element.
We can check the result:
if (result.isPresent()) {
System.out.println(result.get());
}The details of Optional are a separate topic, so for now, just understand that findFirst() safely represents the possibility that no value was found.
Stream Does Not Modify the Original Collection
This is an important point.
Suppose we have:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);Then:
numbers.stream()
.map(number -> number * 2)
.forEach(System.out::println);prints:
20
40
60But the original list is still:
10
20
30A stream processes the data; it doesn't automatically change the original collection.
Stream vs Normal Loop
Both loops and streams can solve many of the same problems.
Using a loop:
for (int number : numbers) {
if (number > 30) {
System.out.println(number);
}
}Using a stream:
numbers.stream()
.filter(number -> number > 30)
.forEach(number -> System.out.println(number));The stream version can be more expressive when several operations need to be chained together.
However, streams aren't automatically better for every situation. A normal loop can sometimes be simpler, especially for straightforward logic.
A Complete Example
Let's say we have a list of student marks and want to find the passing marks, increase them by 5, sort them, and print the results.
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
ArrayList<Integer> marks = new ArrayList<>();
marks.add(45);
marks.add(80);
marks.add(35);
marks.add(90);
marks.add(60);
marks.stream()
.filter(mark -> mark >= 40)
.map(mark -> mark + 5)
.sorted()
.forEach(mark -> System.out.println(mark));
}
}Output:
45
50
65
85
95Here, the stream performs several operations in sequence:
Original marks
↓
Keep passing marks
↓
Add 5
↓
Sort
↓
PrintThat's the real power of the Stream API.
The main thing to remember is:
The Stream API provides a convenient way to process collections of data using operations such as
filter(),map(),sorted(),collect(), andforEach().
Once you become comfortable with lambdas and collections, streams become much easier to understand because they essentially let you describe what you want to do with your data rather than writing every step manually.