Collectors in Java: Grouping and Partitioning
Collectors turn a stream into a collection, a map or a summary, and grouping is where they become genuinely powerful.
- What a Collector does
- To a collection
- Joining
- To a map
- Grouping
- Grouping with a downstream collector
- Choosing the map type
- Multi level grouping
- Partitioning
- Summarising
- Transforming the finished result
- Filtering and flattening inside a group
- A worked report
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What a Collector does
collect is a mutable reduction: it accumulates elements into a container. The Collectors class supplies ready made collectors for almost every shape you need.
To a collection
List<String> list = titles.stream().collect(Collectors.toList());
Set<String> set = titles.stream().collect(Collectors.toSet());
List<String> immutable = titles.stream().collect(Collectors.toUnmodifiableList());
TreeSet<String> sorted = titles.stream()
.collect(Collectors.toCollection(TreeSet::new)); // a chosen implementationJoining
String plain = titles.stream().collect(Collectors.joining());
String commas = titles.stream().collect(Collectors.joining(", "));
String wrapped = titles.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println(wrapped); // [Java loops, SQL joins]To a map
Map<String, Integer> viewsByTitle = notes.stream()
.collect(Collectors.toMap(Note::title, Note::views));
// Duplicate keys throw unless a merge function is supplied
Map<String, Integer> viewsByCategory = notes.stream()
.collect(Collectors.toMap(Note::category, Note::views, Integer::sum));
// Choosing the map implementation
Map<String, Integer> ordered = notes.stream()
.collect(Collectors.toMap(Note::title, Note::views,
(a, b) -> a, LinkedHashMap::new));The two argumenttoMapthrowsIllegalStateExceptionon a duplicate key. This is one of the most common surprises with collectors, and the fix is always to supply a merge function.
Grouping
Map<String, List<Note>> byCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category));
System.out.println(byCategory.get("java").size());The classifier function decides the key, and the values default to a List of the matching elements.
Grouping with a downstream collector
Map<String, Long> countPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category, Collectors.counting()));
Map<String, Integer> viewsPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.summingInt(Note::views)));
Map<String, List<String>> titlesPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.mapping(Note::title, Collectors.toList())));
Map<String, Optional<Note>> topPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.maxBy(Comparator.comparingInt(Note::views))));The second argument transforms each group. This is what makes grouping expressive: the same classifier serves counting, summing, mapping or any other reduction.
Choosing the map type
TreeMap<String, Long> sortedGroups = notes.stream()
.collect(Collectors.groupingBy(Note::category, TreeMap::new,
Collectors.counting()));Multi level grouping
Map<String, Map<Boolean, List<Note>>> byCategoryThenStatus = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.groupingBy(Note::published)));Nesting works to any depth, although two levels is usually the point at which a small record reads better than the nested map type.
Partitioning
Map<Boolean, List<Note>> split = notes.stream()
.collect(Collectors.partitioningBy(Note::published));
List<Note> live = split.get(true);
List<Note> drafts = split.get(false);
Map<Boolean, Long> counts = notes.stream()
.collect(Collectors.partitioningBy(Note::published, Collectors.counting()));groupingBy | partitioningBy | |
|---|---|---|
| Key type | Anything | Boolean only |
| Number of groups | As many as there are distinct keys | Exactly two |
| Empty groups | Absent from the map | Both keys always present |
Both true and false are always present with partitioningBy, even when one side is empty. That guarantee is the practical reason to prefer it over grouping by a boolean.
Summarising
long count = notes.stream().collect(Collectors.counting());
int total = notes.stream().collect(Collectors.summingInt(Note::views));
double mean = notes.stream().collect(Collectors.averagingInt(Note::views));
IntSummaryStatistics stats = notes.stream()
.collect(Collectors.summarizingInt(Note::views));Transforming the finished result
String topTitle = notes.stream()
.collect(Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Note::views)),
best -> best.map(Note::title).orElse("none")));
List<String> frozen = titles.stream()
.collect(Collectors.collectingAndThen(Collectors.toList(), List::copyOf));Filtering and flattening inside a group
// Keeps every category key, even those with no popular note
Map<String, List<Note>> popularPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.filtering(note -> note.views() > 300,
Collectors.toList())));
Map<String, List<String>> tagsPerCategory = notes.stream()
.collect(Collectors.groupingBy(Note::category,
Collectors.flatMapping(note -> note.tags().stream(),
Collectors.toList())));Filtering before groupingBy removes empty groups entirely; Collectors.filtering keeps them with an empty list. Choose according to whether the key should still appear.
A worked report
Map<String, String> report = notes.stream()
.filter(Note::published)
.collect(Collectors.groupingBy(
Note::category,
TreeMap::new,
Collectors.collectingAndThen(
Collectors.summingInt(Note::views),
views -> views + " total views")));
report.forEach((category, summary) -> System.out.println(category + ": " + summary));Common mistakes
- Using
toMapwithout a merge function and hitting a duplicate key. - Assuming
groupingByproduces a sorted map. It returns aHashMapby default. - Expecting a group for a key with no matching elements.
- Using
groupingBywith a boolean classifier instead ofpartitioningBy. - Nesting so many collectors that the type becomes unreadable.
- Calling
toMapwhere a value may benull, which throws.
Best practices
- Always supply a merge function to
toMapunless keys are provably unique. - Use
partitioningByfor a two way split. - Pass
TreeMap::neworLinkedHashMap::newwhen order matters. - Extract a deeply nested collector into a named variable.
- Introduce a record once the result type needs more than two levels of nesting.
Practice
- Group notes by category and count them, then sort the result by key.
- Why does
toMapthrow on duplicate keys, and how do you fix it two ways? - Partition a list of marks into pass and fail and count each side.
- Build a map of category to a comma separated list of titles.
- Explain the difference between filtering before
groupingByand usingCollectors.filtering.
Conclusion
Collectors turn a stream into whatever shape you need. Learn toList, joining, toMap with a merge function, groupingBy with a downstream collector, and partitioningBy, and most reporting code becomes a few readable lines.