Detailed explanation
Collectors.groupingBy() is a collector used with Stream.collect() to classify elements into groups. You supply a classifier function that computes a key for each element; elements whose keys are equal belong to the same group. The result is a Map whose keys represent categories and whose values contain or summarise their members. For example, Map<Integer, List<String>> byLength = Stream.of("a", "bb", "cc", "ddd").collect(Collectors.groupingBy(String::length)); groups strings by length, producing entries equivalent to {1=[a], 2=[bb, cc], 3=[ddd]}. The displayed key order is not guaranteed. This operation is useful for tasks such as grouping employees by department or transactions by customer.
There are three overloads to know. groupingBy(classifier) collects each group's elements into a List. groupingBy(classifier, downstream) lets another collector determine each map value. For example, groupingBy(String::length, Collectors.counting()) produces a Map<Integer, Long> containing the number of strings of each length. Other downstream collectors include summingInt(), averagingDouble(), mapping(), and another groupingBy() for nested groups. mapping() transforms elements within each group before passing them to its own downstream collector; for instance, grouping employees by department while collecting only their names. The third overload, groupingBy(classifier, mapFactory, downstream), controls the result map implementation; TreeMap::new gives sorted keys.
Under the hood, the collector accumulates a separate downstream state for each key. As each element arrives, its classifier result selects the corresponding state, which the downstream collector updates. At completion, downstream finishers, if required, convert those states into final values. In a parallel stream, ordinary groupingBy() can build partial maps and combine them by merging matching groups; it is valid for parallel use but is not a concurrent collector. groupingByConcurrent() supports concurrent accumulation and is unordered, so it should not be chosen when encounter order matters.
Interviewers expect you to connect the classifier, downstream collector, and resulting generic types. Explain that groupingBy() does not guarantee a particular default map implementation or mutable result containers. Standard JDK implementations reject null classifier results, so normalise or filter missing keys. Also distinguish grouping from toMap(): repeated grouping keys are expected and accumulated, rather than requiring an explicit duplicate-key merge function.
Key points to remember
- The classifier computes group keys; equal keys identify the same group.
- The simplest overload returns Map<K, List<T>>.
- A downstream collector changes group values into counts, totals, transformed collections, or nested maps.
- The three-argument overload accepts a map factory, such as TreeMap::new for sorted keys.
- groupingBy() supports parallel reduction; groupingByConcurrent() provides unordered concurrent grouping.
Common mistakes candidates make
- Assuming the default result map preserves insertion order or sorts its keys.
- Declaring counting() results as Integer instead of Long.
- Returning null from the classifier and expecting a null-key group to work.
Likely follow-up questions
How do you preserve the order in which group keys first appear?
For a sequential ordered stream, use groupingBy(classifier, LinkedHashMap::new, Collectors.toList()). This preserves first-seen key order, while the lists retain the stream's encounter order within each group.
How do you create nested groups?
Use groupingBy() as the downstream collector, for example groupingBy(Employee::getDepartment, groupingBy(Employee::getRole)). The result is a Map<Department, Map<Role, List<Employee>>>, assuming those getter return types.
How is groupingBy() different from partitioningBy()?
groupingBy() accepts a classifier and creates groups for keys encountered in the input. partitioningBy() accepts a predicate and returns a Map<Boolean, ...> containing both true and false partitions, even when one is empty.
Interview tip: Start with the string-length example, then add counting() and explain how the result type changes from List<String> to Long.
