Collectors — Practical Demo
Hands-on examples for Collectors. Focuses on
groupingBy,toMap,joining,partitioningBy, and custom collector construction.
Understand the Streams API first — collectors only make sense as the argument to collect(), the terminal operation of a stream pipeline.
Example 1: groupingBy with Downstream Collectors
The most important collector — groups elements by a key and applies a downstream aggregation.
import java.util.*;
import java.util.stream.*;
public class GroupingByDemo {
record Employee(String name, String dept, int salary) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Engineering", 88000),
new Employee("Charlie", "Marketing", 72000),
new Employee("Diana", "Marketing", 80000),
new Employee("Eve", "HR", 65000)
);
// Basic groupBy — Map<String, List<Employee>>
Map<String, List<Employee>> byDept =
employees.stream().collect(Collectors.groupingBy(Employee::dept));
byDept.forEach((dept, emps) ->
System.out.println(dept + ": " + emps.stream().map(Employee::name).toList()));
System.out.println("---");
// Downstream: count per department
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
countByDept.forEach((dept, count) ->
System.out.println(dept + " headcount: " + count));
System.out.println("---");
// Downstream: average salary per department
Map<String, Double> avgSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.averagingInt(Employee::salary)
));
avgSalary.forEach((dept, avg) ->
System.out.printf("%s avg salary: $%.0f%n", dept, avg));
System.out.println("---");
// Downstream: names joined per department, in a sorted map
Map<String, String> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
TreeMap::new, // ← sorted map supplier
Collectors.mapping(
Employee::name,
Collectors.joining(", ") // ← join names
)
));
namesByDept.forEach((dept, names) ->
System.out.println(dept + ": " + names));
}
}
Expected Output:
Engineering: [Alice, Bob]
Marketing: [Charlie, Diana]
HR: [Eve]
---
Engineering headcount: 2
Marketing headcount: 2
HR headcount: 1
---
Engineering avg salary: $91500
HR avg salary: $65000
Marketing avg salary: $76000
---
Engineering: Alice, Bob
HR: Eve
Marketing: Charlie, Diana
groupingBy with a downstream collector is a single-pass operation. Avoid collecting to a Map<K, List<V>> and then doing a second stream pass over the values — compose the downstream collector instead.
Example 2: toMap and partitioningBy
Two collectors that produce structured maps from your data.
import java.util.*;
import java.util.stream.*;
public class ToMapAndPartition {
record Product(String code, String name, double price) {}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("W1", "Widget", 9.99),
new Product("G1", "Gadget", 24.99),
new Product("D1", "Doohickey", 4.99),
new Product("G2", "Gizmo", 14.99)
);
// toMap — code → price lookup
Map<String, Double> priceLookup = products.stream()
.collect(Collectors.toMap(
Product::code, // ← key
Product::price // ← value
// No merge fn — OK as long as codes are unique
));
System.out.println("G1 price: " + priceLookup.get("G1")); // 24.99
// toMap — code → full Product (identity value)
Map<String, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::code, p -> p));
System.out.println("D1: " + productMap.get("D1").name());
// toMap with merge function — handle duplicate product names (keep higher price)
List<Product> withDupes = new ArrayList<>(products);
withDupes.add(new Product("W2", "Widget", 12.99)); // ← same name "Widget"
Map<String, Double> maxPriceByName = withDupes.stream()
.collect(Collectors.toMap(
Product::name,
Product::price,
(existing, replacement) -> Math.max(existing, replacement) // ← merge fn
));
System.out.println("Widget max price: " + maxPriceByName.get("Widget")); // 12.99
System.out.println("---");
// partitioningBy — split into two groups: expensive (> $10) and affordable
Map<Boolean, List<Product>> partitioned = products.stream()
.collect(Collectors.partitioningBy(p -> p.price() > 10));
System.out.println("Expensive: " +
partitioned.get(true).stream().map(Product::name).toList());
System.out.println("Affordable: " +
partitioned.get(false).stream().map(Product::name).toList());
}
}
Expected Output:
G1 price: 24.99
D1: Doohickey
Widget max price: 12.99
---
Expensive: [Gadget, Gizmo]
Affordable: [Widget, Doohickey]
Example 3: Custom Collector
Building a Collector from scratch using Collector.of for a specialized aggregation.
import java.util.*;
import java.util.stream.*;
public class CustomCollectorDemo {
// Custom collector: builds a frequency map (element → count)
public static <T> Collector<T, Map<T, Integer>, Map<T, Integer>> toFrequencyMap() {
return Collector.of(
HashMap::new, // supplier: empty accumulator
(map, element) -> map.merge(element, 1, Integer::sum), // accumulator: fold element
(map1, map2) -> { // combiner: merge two maps (parallel)
map2.forEach((k, v) -> map1.merge(k, v, Integer::sum));
return map1;
}
// No finisher needed: identity transformation (accumulator is already the result)
// Collector.Characteristics.UNORDERED could be added as variance hint
);
}
public static void main(String[] args) {
List<String> words = List.of(
"apple", "banana", "apple", "cherry",
"banana", "apple", "date", "cherry", "cherry"
);
// Use our custom collector
Map<String, Integer> frequency = words.stream()
.collect(toFrequencyMap());
// Print sorted by value descending
frequency.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
System.out.println("---");
// Built-in alternative: Collectors.frequency approach
// (groupingBy + counting yields Map<String, Long>)
Map<String, Long> builtIn = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
System.out.println("Built-in groupingBy counting for apple: " + builtIn.get("apple"));
}
}
Expected Output:
apple: 3
cherry: 3
banana: 2
date: 1
---
Built-in groupingBy counting for apple: 3
When implementing a custom Collector that will be used with parallel streams, always implement the combiner correctly — the default (a, b) -> { throw ... } pattern breaks parallel execution. Also mark the collector with UNORDERED if the result doesn't depend on encounter order.
Exercises
Try these on your own to solidify understanding:
- Easy: Given
List.of("red", "green", "blue", "red", "green", "red"), useCollectors.groupingBy(w -> w, Collectors.counting())to build a frequency map, then print each color and its count. - Medium: Using the
Employeedata from Example 1, collect into aMap<String, Optional<Employee>>where the value is the highest-paid employee per department. UseCollectors.groupingBywithCollectors.maxBy(Comparator.comparingInt(Employee::salary)). - Hard: Implement a custom
Collector<String, ?, String>calledtoSentenceCasethat joins strings with spaces, ensures the first character is uppercase, appends a period at the end, and applies the transformation as the finisher (e.g.,["hello", "world"]→"Hello world.").
Back to Topic
Return to the Collectors note for theory, interview questions, and further reading.