1. Counting elements: count()
When you work with collections of numbers or objects, you almost always need to compute something: the number of elements, a sum, an average, a maximum, or a minimum. For example, the number of employees in a company, the sum of all salaries, the average age of students, the most expensive product, the largest order. With the Stream API, this became simpler and more expressive.
How it works
The count() method is a terminal Stream API operator that returns the number of elements in the stream.
long count = employees.stream().count();
If you only need to count elements that meet a condition, use the filter(...):
long richCount = employees.stream()
.filter(e -> e.getSalary() > 100_000)
.count();
Example for our app:
Suppose there is a list of products:
List<Product> products = List.of(
new Product("Milk", 80),
new Product("Cheese", 250),
new Product("Bread", 40)
);
Let’s count how many products cost more than 100:
long expensiveCount = products.stream()
.filter(p -> p.getPrice() > 100)
.count();
System.out.println("Expensive products: " + expensiveCount);
What is Optional
Java has a special container—Optional. It’s a wrapper object that may contain a value or be empty. It’s used when a result may be absent and you want to make that explicit. For example, the methods min(), max(), and average() can return an empty result if the collection is empty. Instead of null an Optional is returned.
Main Optional methods
- isPresent() / isEmpty() — check whether a value is present.
- orElse(value) — return the value or a default if empty.
- orElseThrow() — throw an exception if empty.
- ifPresent(...) — perform an action only if a value is present.
Example:
OptionalInt min = products.stream()
.mapToInt(Product::getPrice)
.min();
if (min.isPresent()) {
System.out.println("Minimum price: " + min.getAsInt());
} else {
System.out.println("List is empty");
}
Or shorter:
int minValue = min.orElse(-1);
System.out.println("Minimum price: " + minValue);
This approach helps you avoid NullPointerException and makes your code safer.
2. Sum, average, minimum, and maximum for numeric data
Primitive streams: IntStream, DoubleStream, LongStream
For numeric data, the Stream API offers special streams: IntStream, DoubleStream, LongStream. They make it easy and fast to compute sum, average, minimum, and maximum.
Transforming to a numeric stream
To get a numeric stream from a stream of objects, use mapToInt, mapToDouble, mapToLong.
int sum = products.stream()
.mapToInt(Product::getPrice)
.sum();
System.out.println("Total price sum: " + sum);
Methods
- sum() — the sum of all elements.
- average() — the arithmetic mean (returns OptionalDouble).
- min(), max() — minimum and maximum (return OptionalInt/OptionalDouble/OptionalLong).
Examples:
// Sum of all prices
int total = products.stream()
.mapToInt(Product::getPrice)
.sum();
// Average price
OptionalDouble avg = products.stream()
.mapToInt(Product::getPrice)
.average();
// Minimum and maximum price
OptionalInt min = products.stream()
.mapToInt(Product::getPrice)
.min();
OptionalInt max = products.stream()
.mapToInt(Product::getPrice)
.max();
How to get a value from Optional
double average = avg.orElse(0.0); // 0.0 if the collection is empty
int minValue = min.orElse(-1); // -1 if the collection is empty
int maxValue = max.orElse(-1); // -1 if the collection is empty
Programmer’s joke: an empty list is not an error—it’s just a situation where you have no data. But if you try to take a value from an empty Optional with getAsInt(), you’ll get a NoSuchElementException. Always use orElse or isPresent()!
3. Aggregating objects: Collectors.summingInt, averagingInt, maxBy, minBy
If you have a stream of objects and want to aggregate values by some attribute, use special collectors from the Collectors class.
summingInt, averagingInt
int totalSalary = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary));
double avgSalary = employees.stream()
.collect(Collectors.averagingInt(Employee::getSalary));
minBy, maxBy
To find the object with the maximum/minimum value of a field:
Optional<Employee> richest = employees.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));
Optional<Employee> poorest = employees.stream()
.collect(Collectors.minBy(Comparator.comparingInt(Employee::getSalary)));
Important: the result is an Optional because the collection may be empty.
Example
Let’s create a Product class:
public class Product {
private final String name;
private final int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public int getPrice() { return price; }
}
Create a list of products:
List<Product> products = List.of(
new Product("Milk", 80),
new Product("Cheese", 250),
new Product("Bread", 40)
);
Find the most expensive product:
Optional<Product> maxProduct = products.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Product::getPrice)));
maxProduct.ifPresent(p -> System.out.println("The most expensive product: " + p.getName()));
4. Combining with grouping
Aggregating methods are often combined with grouping. For example, you can find the average price of products by the first letter of the name:
Map<Character, Double> avgPriceByLetter = products.stream()
.collect(Collectors.groupingBy(
p -> p.getName().charAt(0),
Collectors.averagingInt(Product::getPrice)
));
System.out.println(avgPriceByLetter);
Result:
{M=80.0, C=250.0, B=40.0}
5. Real-world examples
Example 1: Students and grades
A list of students and their scores:
public class Student {
private final String name;
private final int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
List<Student> students = List.of(
new Student("Alice", 90),
new Student("Bob", 75),
new Student("Charlie", 100)
);
How many students have a score above 80?
long count = students.stream()
.filter(s -> s.getScore() > 80)
.count();
System.out.println("Students with score > 80: " + count);
Average score:
double avg = students.stream()
.mapToInt(Student::getScore)
.average()
.orElse(0.0);
System.out.println("Average score: " + avg);
The top student:
students.stream()
.max(Comparator.comparingInt(Student::getScore))
.ifPresent(s -> System.out.println("Top student: " + s.getName()));
Example 2: Number of unique products
long uniqueCount = products.stream()
.map(Product::getName)
.distinct()
.count();
System.out.println("Unique products: " + uniqueCount);
6. Useful nuances
Visual diagram: how aggregating methods work
Collection of objects
│
▼
stream()
│
▼
mapToInt/Double/Long (if needed)
│
▼
sum() / average() / min() / max() / count()
│
▼
Result (int, double, long, Optional)
Handling Optional: how to safely extract values
Why do methods return Optional?
If the collection is empty (for example, you’re searching for a maximum among an empty list), an empty Optional is returned. If you immediately call getAsInt()/get(), an exception will be thrown.
The right way:
- Use orElse(default value)
- Use ifPresent(...) to perform actions only when a value is present
- Use orElseThrow(...) to explicitly throw an exception
Example:
OptionalDouble avg = products.stream()
.mapToInt(Product::getPrice)
.average();
System.out.println("Average price: " + avg.orElse(0.0));
When to use primitive streams vs. Collectors
- If you just need to compute sum/average/min/max over a numeric field, use mapToInt and the corresponding method (sum, average, min, max).
- If you want to aggregate objects (for example, find the object with the maximum field), use the collectors maxBy/minBy.
- If you need to group and compute aggregates per group, use a combination of groupingBy with aggregating collectors.
7. Typical mistakes when working with aggregating methods
Error #1: Optional is not handled. Beginners often forget that min, max, average return an Optional. As a result, when trying to get a value from an empty container, a NoSuchElementException occurs. Always use orElse, orElseThrow, or ifPresent.
Error #2: Using a regular stream instead of mapToInt/mapToDouble. If you write stream().sum(), the compiler will say: “There is no such method!”. For sum, average, minimum, and maximum you need primitive streams.
Error #3: Type mismatch in comparisons. When you search for max/min among objects, use the correct comparator: Comparator.comparingInt(Employee::getSalary). Otherwise, you can get a compilation error or an incorrect result.
Error #4: Comparing objects with null. If the collection may contain null elements, aggregation can throw a NullPointerException. It’s better to filter such elements in advance or use Comparator.nullsLast(...)/nullsFirst(...).
Error #5: Using sum/average/min/max with empty collections without considering the result. If the collection is empty, sum() returns 0, and average()/min()/max() return an empty Optional. Don’t forget to handle this case.
GO TO FULL VERSION