Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 21

Collection Grouping and Partitioning with Built-in Collectors

Recommended resources

Hello! Welcome to our next lesson on functional programming in Java.

In our previous lessons, we've journeyed from creating streams to performing transformations with map, filter, and flatMap. You've mastered how to change the shape and content of a stream, and even how to flatten nested collections into a single, processable sequence. Now, we'll focus on the final step in many data processing pipelines: aggregation. How do you take a flat stream of data and organize it into meaningful groups?

Today, we'll focus on a powerful capability that is frequently a topic in technical interviews. Our goal is to learn how to group and partition collection data effectively using built-in collectors like groupingBy and partitioningBy. Think of this as the Java equivalent of SQL's GROUP BY clause, allowing you to perform sophisticated data analysis and summarization directly in your code.

1. Grouping Data with Collectors.groupingBy

The groupingBy collector is the primary tool for classifying stream elements into categories. It takes a stream of objects and returns a Map, where the keys are the result of a classification function you provide, and the values are Lists of the objects that fall into that category.

Let's consider a classic interview scenario: you have a list of Employee objects and you need to group them by their department.

// Let's assume this Employee class for our examples
class Employee {
    private int id;
    private String name;
    private String department;
    private double salary;

    // constructor, getters, etc.
}

List<Employee> employees = //... a list of employees

Map<String, List<Employee>> employeesByDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

In this simple, yet powerful, one-liner:

  • Employee::getDepartment is the classifier function. For each employee in the stream, this function is called, and its return value (the department String) is used as the key in the resulting map.
  • The groupingBy collector then gathers all Employee objects that produce the same key into a List.

The result is a Map<String, List<Employee>>, exactly what we need.

Visualizing Collectors.groupingBy() Operation
This diagram illustrates the flow: a stream of elements (`T`) is processed by a classifier function, which produces keys (`R`). The collector then organizes the elements into a `Map` where each key maps to a collection of the corresponding elements.

The Overloads of groupingBy

The real power of groupingBy, and a key area for senior-level interviews, lies in its overloaded methods. These allow you to customize both the values in the map and the map's implementation itself.

To see how this works in practice, let's watch a video that walks through a similar problem: grouping employees by age.

Java 8 Stream Collectors groupingBy Example | Java 8 coding Interview Questions & Answer|Code Decode

The video 'Java 8 Stream Collectors groupingBy Example' by Code Decode is an excellent resource that covers the three main ways to use groupingBy. It starts with the basic case and progressively builds up to the more advanced uses you'd be expected to know in an interview.

First, watch from 02:38 to 04:05. This part demonstrates the basic, single-argument groupingBy collector, which groups employees by age into a Map<Integer, List<Employee>>. This is the foundation for everything else.

Level 2: groupingBy with a Downstream Collector

What if you don't want a List<Employee> as the value? Maybe you just want the count of employees per department, or a Set of their names. This is where the second argument, the downstream collector, comes in. It allows you to perform a further reduction on the elements within each group.

Example 1: Counting elements in each group

Map<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment, 
        Collectors.counting() // Downstream collector
    ));

// Output: {HR=5, Engineering=12, Marketing=3}

Example 2: Mapping elements to another type within each group

Let's group by department, but the value should be a List of just the employee names.

Map<String, List<String>> namesByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.mapping(Employee::getName, Collectors.toList()) // Downstream collector
    ));

// Output: {HR=[Alice, Bob, ...], Engineering=[Charlie, David, ...]}

Here, Collectors.mapping() acts as a nested collector, first applying the Employee::getName function to each employee in the group and then collecting those names into a List.

Now, let's see how you can change the collection type from a List to a Set to store unique elements.

Java 8 Stream Collectors groupingBy Example | Java 8 coding Interview Questions & Answer|Code Decode

Continuing with the same video, let's see how a downstream collector (toSet()) is used to handle duplicates.

Watch from 06:47 to 10:33. This segment explains how to use the two-parameter groupingBy method to collect the values into a Set instead of the default List. This is a common requirement to eliminate duplicates within a group.

Level 3: Specifying the Map Implementation

By default, groupingBy uses a HashMap. What if your interviewer asks for the results to be sorted by the key (e.g., department name)? You can use the third overload of groupingBy to provide a Supplier for the map implementation you want, such as a TreeMap.

Map<String, List<Employee>> sortedEmployeesByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment, 
        TreeMap::new, // Map supplier
        Collectors.toList() // Downstream collector
    ));

Now, the resulting map will have its keys (departments) sorted alphabetically.

Java 8 Stream Collectors groupingBy Example | Java 8 coding Interview Questions & Answer|Code Decode

Finally, let's look at the third overload of groupingBy which gives you full control.

Watch from 10:56 to 15:24. This section demonstrates the three-parameter version of groupingBy, allowing you to specify the map implementation (like TreeMap for sorted output). It also neatly summarizes how the simpler overloads are just conveniences that call the more complex ones internally.

Test your understanding!

Given the List<Employee> employees, write a stream pipeline to produce a Map<String, Double> where the key is the department and the value is the average salary for that department.

Hint: Look for a downstream collector that can calculate summary statistics.

Show answer
import java.util.stream.Collectors;
import java.util.Map;
import java.util.List;

// ... Assuming Employee class is defined

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

System.out.println(avgSalaryByDept);

Explanation: We group by department. The downstream collector, Collectors.averagingDouble(), is applied to the employees in each group. It takes a function to extract the double value (the salary) from each employee and calculates the average.


2. Partitioning Data with Collectors.partitioningBy

partitioningBy is a special case of groupingBy. While groupingBy can create many groups based on a classifier function, partitioningBy is used when you only need to split the stream into two groups based on a Predicate (a condition that returns true or false).

The result is always a Map<Boolean, List<T>>: one list for elements that satisfy the predicate (true), and one for those that don't (false).

Visualizing Collectors.partitioningBy() in Java Streams
This diagram shows how a stream is split into two distinct groups based on a boolean predicate. All elements matching the predicate go into the `true` list, and all others go into the `false` list.

Let's read a concise guide to understand partitioningBy.

Java 8 Streams: Definitive Guide to partitioningBy()

The article 'Java 8 Streams: Definitive Guide to partitioningBy()' from StackAbuse provides a clear and focused explanation of this collector.

Read the sections 'Guide to Collectors.partitioningBy()' and 'Collectors.partitioningBy() using a Predicate'. Focus on how it always returns a Map<Boolean, List<T>> and how the predicate is used to divide the stream.

Example: Partitioning employees by salary

Let's partition our employees into two groups: high earners (salary > 100,000) and everyone else.

Map<Boolean, List<Employee>> partitionedBySalary = employees.stream()
    .collect(Collectors.partitioningBy(e -> e.getSalary() > 100_000));

List<Employee> highEarners = partitionedBySalary.get(true);
List<Employee> otherEarners = partitionedBySalary.get(false);

Just like groupingBy, partitioningBy also has an overload that accepts a downstream collector. For instance, we could partition employees and get just the count in each group.

Java 8 Streams: Definitive Guide to partitioningBy()

Now let's see how to add a downstream collector to partitioningBy.

Read the section 'Collectors.partitioningBy() using a Predicate and a Downstream Collector'. Pay attention to the examples using Collectors.mapping() and Collectors.counting() to perform further aggregation on the partitioned groups.


3. Interview Corner: groupingBy vs. partitioningBy

A common interview question is to explain the difference between these two collectors. Being able to articulate this clearly shows a solid grasp of the Stream API.

Here’s your cheat sheet:

  1. Input Function: groupingBy takes a Function (the classifier). partitioningBy takes a Predicate.
  2. Number of Groups: groupingBy can create an arbitrary number of groups, one for each unique key returned by the classifier. partitioningBy always creates exactly two groups, represented by the boolean keys true and false.
  3. Output Keys: The keys in the map from groupingBy are of type K (the return type of the classifier). The keys from partitioningBy are always Boolean.
  4. Completeness: partitioningBy will always return a map with entries for both true and false, even if one or both of the corresponding lists are empty. groupingBy only creates entries for keys that are actually present in the data.

Java 8 Streams: Definitive Guide to partitioningBy()

To solidify this understanding, read the direct comparison in the StackAbuse article.

Read the section 'Similarities and Differences Between partitioningBy() And groupingBy()'. This section summarizes the key distinctions, which is perfect for interview preparation.

In short: if you need to classify data into more than two categories (e.g., by department, city, or status), use groupingBy. If you need to split data into two buckets based on a simple yes/no condition (e.g., passing/failing, active/inactive), partitioningBy is the more specific and potentially more efficient choice.


Conclusion

You have now mastered two of the most powerful collectors in the Java Stream API. These tools allow you to move beyond simple transformations and perform complex, SQL-like aggregations with clean, functional code.

Key Takeaways for Your Interview:

  • groupingBy is for multi-way classification. Use it when you need to group data by properties like department, category, or type.
  • partitioningBy is for a two-way split. Use it for simple boolean conditions like isEven, isPassing, or isActive.
  • Downstream collectors are key. Show your depth by using downstream collectors like counting(), mapping(), summarizingDouble(), and reducing() to perform advanced aggregations within each group.
  • Be ready to explain the difference. Clearly articulate that groupingBy uses a Function to create N groups, while partitioningBy uses a Predicate to create exactly two (true/false) groups.

Preview of the Next Lesson:
So far, our streams have always contained data. But in real-world systems, built with Spring Boot and microservices, data is often missing or null. How do you handle this gracefully without littering your code with null checks? In our next lesson, we will learn how to write idiomatic, null-safe code using the Optional class, a core concept in modern Java development.