A terminal operation of a Stream allows you to materialize, group, or reduce the processed elements.
In the previous article, we saw how to set up the pipeline: .filter() to remove what we don’t want and .map() to transform data.
But we ended up using .forEach(), which is only useful for printing or executing actions.
In the real world, you don’t want to print data. You want to obtain a new list with the processed data to continue working with it. Or you want to group them. Or you want to sum them all.
Today we’ll see how to materialize Stream results.
Collecting results (collect)
collect is the most versatile terminal operation. It takes all the elements that have survived the stream and “packages” them into a data structure.
To use it, we need the Collectors utility class, which provides the packaging recipes.
To List (toList)
This is the most common use case (90% of cases).
// Java 8 to 15:
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
// Java 16+ (Much better):
// You can call .toList() directly without using Collectors
List<String> names = users.stream()
.map(User::getName)
.toList(); // Note: Returns an IMMUTABLE list
To Set (toSet)
Useful if you want to automatically remove duplicates when the process finishes.
Set<String> uniqueCountries = users.stream()
.map(User::getCountry)
.collect(Collectors.toSet());
Joining Text (joining)
Do you want to convert a list of names into a single String separated by commas?
String result = users.stream()
.map(User::getName)
.collect(Collectors.joining(", "));
// Output: "Alice, John, Luis"
Grouping data (groupingBy)
One of the most useful operations is groupingBy.
If you come from SQL, this is a GROUP BY.
If you come from old Java, you know the nightmare of creating a Map<String, List<User>>, iterating, checking if the key exists, creating the list if it doesn’t, adding…
With Streams, it’s one line.
Imagine we want to group users by City.
// The result will be a Map:
// Key = City (String)
// Value = List of users in that city (List<User>)
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
Visual result of the Map:
{
"Madrid": [User(Alice), User(Luis)],
"Barcelona": [User(John)],
"Valencia": [User(Sara), User(Elena)]
}
We can also group and count (How many users are in each city?):
Map<String, Long> countByCity = users.stream()
.collect(Collectors.groupingBy(User::getCity, Collectors.counting()));
// { "Madrid": 2, "Barcelona": 1 ... }
Reduction (reduce)
Sometimes you don’t want a list; you want a single value that summarizes everything.
- The sum of all salaries.
- The highest number.
- The concatenation of all letters.
reduce takes a sequence of elements and combines them repeatedly until only one remains.
It requires two things:
- Initial Value (Identity): The neutral value (0 for sum, 1 for multiplication, "" for text).
- Accumulator (BinaryOperator): A function that takes two elements and returns one.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Sum all numbers
Integer sum = numbers.stream()
.reduce(0, (accumulated, current) -> accumulated + current);
// Process:
// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6 ... Final result: 15
Using method references makes it look beautiful:
Integer sum = numbers.stream().reduce(0, Integer::sum);
Primitive Streams (IntStream)
If you are going to work with many numbers, using Stream<Integer> is inefficient (due to boxing/unboxing of objects).
Java has special Streams for primitive types: IntStream, DoubleStream, LongStream.
// mapToInt converts the object stream to a pure IntStream
int totalSum = users.stream()
.mapToInt(User::getAge) // Now 'int' values flow, not 'Integer'
.sum(); // Direct methods: sum(), average(), max(), min()
Statistics in one go:
If you use primitives, you can call .summaryStatistics() and it gives you the max, min, average, and sum all at once.
Complete example: the e-commerce
Let’s combine everything. We have a list of Order. We want to know how much money we have earned in total for each product category.
// 1. Group by category
Map<String, Double> totalByCategory = orders.stream()
// Group by category
.collect(Collectors.groupingBy(
Order::getCategory,
// Instead of storing the list of orders, we sum their amounts
Collectors.summingDouble(Order::getAmount)
));
// Result: { "Electronics": 5000.0, "Clothing": 1200.50 }