Definition
- Collectors.groupingBy(classifier, downstream) partitions stream elements by a classifier and applies a downstream collector to each group.
- Collectors.counting() counts elements in each group, returning
Long. - The result type is
Mapwhere each value is the head count. - counting() never materialises the group members: it increments a running total.
- Use the three-argument form with a
TreeMap::newsupplier for sorted department keys.
Priya at the National Bank of India sorts employee files into department trays, then simply counts how many files are in each tray. The tray label is the department and the number written on it is the head count.
Understanding the Concepts
How do you count employees per department?
groupingBy with counting() is the canonical way to produce a count-per-group map. The classifier extracts the department, and the counting downstream tallies each group.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class DepartmentCount {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", "Loans"),
new Employee("Priya", "IT"),
new Employee("Amit", "Loans"),
new Employee("Kavita", "IT"),
new Employee("Neha", "Finance"),
new Employee("Suresh", "IT")
);
Map<String, Long> deptCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()));
deptCount.forEach((dept, count) ->
System.out.println(dept + ": " + count));
}
}
Loans: 2
IT: 3
Finance: 1
How is this different from grouping into lists and taking size?
You could group into lists and then compute the size, but counting() is more efficient because it never stores the group members.
| Approach | Memory | What is stored per group |
|---|---|---|
| groupingBy(dept, counting()) | Low | Running count (long) |
| groupingBy(dept, toList()) then .size() | High | All employee objects |
// Verbose: groups into lists first, then maps to sizes
Map<String, Integer> verbose = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment))
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().size()));
// Clean and efficient: counting downstream
Map<String, Long> clean = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()));
How do you get sorted department names?
The default map is HashMap (unordered). Supply a TreeMap supplier for alphabetical keys.
Map<String, Long> sorted = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
TreeMap::new,
Collectors.counting()));
sorted.forEach((dept, count) ->
System.out.println(dept + ": " + count));
Finance: 1
IT: 3
Loans: 2
What other downstream collectors can you use instead of counting?
The two-argument groupingBy with a downstream collector is the general pattern that unlocks many per-group analytics.
| Downstream collector | Result type | Purpose |
|---|---|---|
| counting() | Long | Count per group |
| summingDouble(salary) | Double | Total salary per group |
| averagingDouble(salary) | Double | Average salary per group |
| maxBy(comparing(salary)) | Optional
| mapping(getName, toList()) | List
| summarizingDouble(salary) | DoubleSummaryStatistics | All stats per group |
What are common mistakes?
Mistake: expecting Integer instead of Long from counting().
// WRONG: counting() returns Long, not Integer
Map<String, Integer> wrong = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting())); // compile error: Long vs Integer
// CORRECT: use Long as the value type
Map<String, Long> correct = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()));
How do you count by a derived key?
The classifier can be any function, not just a simple getter. You can derive the key.
// Count by salary band
Map<String, Long> byBand = employees.stream()
.collect(Collectors.groupingBy(
e -> e.getSalaryAsDouble() >= 80000 ? "Senior" : "Junior",
Collectors.counting()));
What should a senior engineer know?
- counting() returns Long deliberately to avoid integer overflow on very large groups.
- The downstream collector runs per group, not per element. For n elements in k groups, counting() does n increment operations.
- In parallel streams, counting() is split-friendly because addition is associative.
- For a simple one-argument groupingBy (no downstream), the default downstream is toList().
What are common interview follow-up questions?
- "How do you find the department with the most employees?" Chain with entrySet().stream().max(comparingByValue()).
- "How do you filter departments with more than 2 employees?" Stream the resulting map and filter by value.
- "How do you count by multiple criteria?" Use a composite key or nested groupingBy.
Visual Overview
Stream<Employee>
[Rajesh:Loans, Priya:IT, Amit:Loans, Kavita:IT, Neha:Finance, Suresh:IT]
│
│ groupingBy(getDepartment, counting())
▼
Partitioning:
Loans → [Rajesh, Amit] → count: 2
IT → [Priya, Kavita, Suresh] → count: 3
Finance → [Neha] → count: 1
│
▼
Map<String, Long>
┌────────────────────┐
│ Loans → 2 │
│ IT → 3 │
│ Finance → 1 │
└────────────────────┘
Key Takeaway
To count employees per department, collect with groupingBy using the department as the classifier and Collectors.counting() as the downstream. This partitions employees into groups and counts each group without storing the members, making it both clean and memory-efficient.
The two-argument groupingBy with a downstream collector is the general pattern that also unlocks summing, averaging, and mapping per group. At the National Bank of India, the staffing report used this for a head-count-per-department view in a single pass.
The default map is unordered HashMap. For sorted department names, use the three-argument form with a TreeMap supplier. counting() returns Long (not Integer) to stay safe on very large groups.
Remember one sentence: groupingBy(classifier, counting()) produces a group-to-count map in one pass, and counting() returns Long.
---