Definition
- Two-argument
groupingBy(classifier, downstream)replaces the default list collector with a custom downstream. counting()counts elements per group, producingMap.- Memory-efficient: never materialises per-group lists; only keeps running counts.
- Sorted keys:
groupingBy(classifier, TreeMap::new, counting())for alphabetical order. - Alternative:
toMap(getDept, e -> 1L, Long::sum)but less readable.
Instead of keeping a tray of cards per department, Priya only wants a tally: how many people are in each department. The counting downstream is that per-department tally.
Understanding the Concepts
How do you count employees per department?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class CountByDepartment {
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Rajesh", "Loans", new BigDecimal("95000")),
new Employee("Priya", "IT", new BigDecimal("120000")),
new Employee("Amit", "Finance", new BigDecimal("85000")),
new Employee("Kavita", "Loans", new BigDecimal("78000")),
new Employee("Suresh", "IT", new BigDecimal("110000")));
Map<String, Long> counts = staff.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()));
counts.forEach((dept, count) ->
System.out.println(dept + ": " + count));
}
}
Loans: 2
IT: 2
Finance: 1
Why is counting() more efficient than grouping then sizing?
| Approach | Memory | Steps |
|---|---|---|
| groupingBy(dept, counting()) | Only counts per group | One pass |
| groupingBy(dept) then map to size | Full lists per group | Two passes, more memory |
counting() never materialises the per-group lists: it only keeps a running counter per group.
How do you get sorted department keys?
Map<String, Long> sorted = staff.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
TreeMap::new,
Collectors.counting()));
// Keys: Finance, IT, Loans
What are common mistakes?
Mistake: grouping into lists and then calling size, wasting memory.
// WRONG: builds full lists just to count them
Map<String, List<Employee>> groups = staff.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
groups.forEach((dept, list) -> System.out.println(dept + ": " + list.size()));
// CORRECT: counting downstream avoids materialising lists
Map<String, Long> counts = staff.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment, Collectors.counting()));
What about filtering departments with count > 1?
counts.entrySet().stream()
.filter(e -> e.getValue() > 1)
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
What about the toMap alternative?
// Alternative: toMap with merge function
Map<String, Long> alt = staff.stream()
.collect(Collectors.toMap(
Employee::getDepartment,
e -> 1L,
Long::sum));
// Works, but groupingBy + counting is more readable
What about counting with a different downstream?
The pattern extends to any downstream: summingDouble, averagingDouble, maxBy, mapping, etc.
// Sum of salaries per department
Map<String, Double> salarySum = staff.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summingDouble(e -> e.getSalary().doubleValue())));
What should a senior engineer know?
- counting() returns Long, not int, to avoid overflow for very large groups.
- groupingBy with a downstream collector is a one-pass operation.
- The same pattern works for any classifier: city, age range, year of joining.
- For concurrent counting, use
groupingByConcurrent(dept, counting()).
What are common interview follow-up questions?
- "How do you find the department with the most employees?" `max(comparingByValue())` on the entry set. - "How do you sort by count descending?" Stream the entry set and sort by value reversed. - "Can you do this without groupingBy?" Yes, with `toMap(dept, e -> 1L, Long::sum)`, but it is less clear.
Visual Overview
List<Employee>
[Rajesh/Loans, Priya/IT, Amit/Finance, Kavita/Loans, Suresh/IT]
│
│ collect(groupingBy(dept, counting()))
│
│ Loans: +1 +1 = 2
│ IT: +1 +1 = 2
│ Finance: +1 = 1
▼
Map<String, Long>
┌──────────────────┐
│ Loans → 2 │
│ IT → 2 │
│ Finance → 1 │
└──────────────────┘
Key Takeaway
To count employees per department I use the two-argument groupingBy with Collectors.counting() as the downstream. Instead of collecting each group into a list, the counting downstream tallies how many employees fall into each group, producing a Map of department to Long count.
This never materialises per-group lists, so it uses far less memory than grouping into lists and then calling size. For sorted keys I use the three-argument form with TreeMap::new.
In my banking project at the National Bank of India this fed an HR capacity-planning dashboard showing head count per department. The same pattern extends to any downstream: summingDouble, averagingDouble, maxBy.
Remember one sentence: groupingBy(dept, counting()) counts employees per department in one pass without materialising per-group lists.
---