Back to Stream: Grouping & Partitioning
Q7Question 7 of 22

Partition employees into two groups: salary > 50,000 and salary <= 50,000 using Collectors.partitioningBy

Key Concepts
  • Collectors.partitioningBy(predicate) splits a stream into exactly two groups by a boolean test.
  • The result is Map<Boolean, List<Employee>> with both true and false keys always present.
  • Unlike groupingBy, a partition key is never missing, even when one side is empty.
  • Read results with result.get(true) and result.get(false), never worrying about null keys.
  • partitioningBy accepts a downstream collector, like partitioningBy(pred, counting()).
  • Use BigDecimal.compareTo for the money predicate, never == or double comparison.
  • partitioningBy is marginally more efficient because it uses a fixed two-slot structure.

Definition

`partitioningBy(e -> e.getSalary().compareTo(FIFTY_K) > 0)` always yields a two-key map, `true` and `false`, never missing either side.

Collectors.partitioningBy splits a stream into exactly two groups based on a boolean predicate.

  • partitioningBy(predicate) – returns Map> with precisely two keys, true and false.
  • Both keys always present – even if one group is empty, its key maps to an empty list, never to null.
  • Specialised grouping – it is a more efficient form of grouping for a binary split.
  • Downstream aware – the two-argument form reduces each partition, for example partitioningBy(pred, counting()).
  • Difference from groupingBy – grouping only creates a key when an element produces it; partitioning guarantees both.

Priya at the National Bank of India has one simple question for each staff card: does this person earn more than 50,000, yes or no? She keeps two trays, a yes tray and a no tray, and drops each card into one of them. Partitioning is grouping when there are exactly two buckets decided by a true-or-false test.

Understanding the Concepts

What is Collectors.partitioningBy?

Collectors.partitioningBy takes a Predicate and routes each element to the true bucket if the predicate holds and the false bucket otherwise. The result is always a Map> with both keys present.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SalarySplitter {
    private static final BigDecimal FIFTY_K = new BigDecimal("50000");

    public static Map<Boolean, List<Employee>> splitBySalary(List<Employee> employees) {
        return employees.stream()
                .collect(Collectors.partitioningBy(
                        e -> e.getSalary().compareTo(FIFTY_K) > 0));
    }
}

Why does partitioningBy exist when groupingBy can do the same?

You could group by a boolean classifier, but partitioning is the dedicated tool for a binary split. It guarantees both keys are present, so reading code never has to defend against a missing side, and internally it uses a fixed two-slot structure instead of a general hash map.

JAVA
// groupingBy with a boolean classifier: only creates keys that occur.
Map<Boolean, List<Employee>> grouped = employees.stream()
        .collect(Collectors.groupingBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));
// If no employee is above 50,000, there is NO true key at all.

// partitioningBy: both true and false always exist.
Map<Boolean, List<Employee>> partitioned = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));
// true maps to an empty list rather than being absent.

How do you read the two buckets?

Reading is just result.get(true) for the employees above 50,000 and result.get(false) for everyone at or below 50,000. Because both keys are guaranteed, there is no null check.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public class Employee {
    private final String name;
    private final BigDecimal salary;

    public Employee(String name, BigDecimal salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() { return name; }
    public BigDecimal getSalary() { return salary; }

    @Override public String toString() { return name; }
}

How do you run a full example?

JAVA
package com.bank.service;

import com.bank.model.Employee;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SalarySplitterDemo {
    private static final BigDecimal FIFTY_K = new BigDecimal("50000");

    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee("Rajesh", new BigDecimal("75000")),
                new Employee("Priya", new BigDecimal("50000")),
                new Employee("Amit", new BigDecimal("42000")),
                new Employee("Pankaj", new BigDecimal("120000")));

        Map<Boolean, List<Employee>> split = employees.stream()
                .collect(Collectors.partitioningBy(
                        e -> e.getSalary().compareTo(FIFTY_K) > 0));

        System.out.println("Above 50,000: " + split.get(true));
        System.out.println("50,000 or less: " + split.get(false));
    }
}
BASH
javac com/bank/model/Employee.java com/bank/service/SalarySplitterDemo.java
java com.bank.service.SalarySplitterDemo
TEXT
Above 50,000: [Rajesh, Pankaj]
50,000 or less: [Priya, Amit]

Note that Priya at exactly 50,000 lands in the false bucket because the predicate is strictly greater than.

How is partitioningBy different from groupingBy?

The core difference is the number of keys and their guaranteed presence.

| Aspect | groupingBy | partitioningBy |
|--------|-----------|----------------|
| Classifier | any function returning any key | a boolean predicate |
| Number of keys | any number, data driven | exactly two: true and false |
| Missing key possible | yes, if no element produces it | no, both always present |
| Result type | Map> | Map> |
| Internal structure | general hash map | fixed two-slot structure |

JAVA
// groupingBy by department: keys depend on the data.
Map<String, List<Employee>> byDept = employees.stream()
        .collect(Collectors.groupingBy(e -> departmentOf(e)));

// partitioningBy by a rule: always two keys.
Map<Boolean, List<Employee>> byRule = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));

How do you add a downstream collector?

Like groupingBy, partitioningBy accepts a downstream collector. partitioningBy(predicate, counting()) gives Map of how many fall on each side.

JAVA
Map<Boolean, Long> countPerSide = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0,
                Collectors.counting()));
TEXT
{false=2, true=2}

What are common mistakes?

The biggest money mistake is comparing BigDecimal with == or converting to double. Use compareTo. A second mistake is assuming the boundary value lands on the wrong side: 50,000 exactly is not greater than 50,000, so it goes to false.

JAVA
// WRONG: == compares object references, not numeric value.
// e -> e.getSalary() == new BigDecimal("50000")   // always false

// WRONG: double loses precision for money.
// e -> e.getSalary().doubleValue() > 50000.0

// CORRECT: compareTo for exact numeric comparison.
Map<Boolean, List<Employee>> correct = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));

What are the edge cases?

An empty stream still returns a map with both keys, each mapping to an empty list. A predicate that is true for every element leaves the false key mapped to an empty list, and vice versa. The boundary value depends on whether you use > or >=.

JAVA
// Edge case: empty input still has both keys.
Map<Boolean, List<Employee>> emptyResult = List.<Employee>of().stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));
// emptyResult = {false=[], true=[]}
System.out.println(emptyResult.get(true).isEmpty());  // true, no NPE

When should you use partitioningBy, and when not?

Use partitioningBy whenever the split is genuinely binary and driven by a true-or-false business rule, because the guaranteed both-keys property removes null handling. Do not use it when you have more than two categories, where groupingBy fits, and do not use it if a boolean key would be awkward to read downstream, where a named enum classifier with groupingBy is clearer.

JAVA
// Use partitioningBy: a true/false rule.
Map<Boolean, List<Employee>> eligible = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0));

// Do NOT use partitioningBy: three tiers need groupingBy.
// LOW, MID, HIGH -> use an enum classifier with groupingBy.

What are common interview follow-up questions?

Interviewers ask why partitioning guarantees both keys, how it differs from grouping on a boolean, how to count or reduce each side, where the boundary value lands, and why BigDecimal.compareTo is required for money. A senior answer adds that partitioningBy is slightly faster because it avoids boxing a Boolean into a general map and uses a fixed two-slot container.

JAVA
// Follow-up: total salary on each side using a reducing downstream.
import java.math.BigDecimal;

Map<Boolean, BigDecimal> totalPerSide = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(FIFTY_K) > 0,
                Collectors.reducing(BigDecimal.ZERO,
                        Employee::getSalary, BigDecimal::add)));

Visual Overview

DIAGRAM
  List<Employee>
       │
       │  partitioningBy(e -> salary > 50000)
       ▼
  ┌───────────────── for each element ─────────────────┐
  │   predicate true?                                    │
  │        │                                             │
  │    ┌───┴────┐                                         │
  │    ▼        ▼                                         │
  │  true     false                                      │
  │  bucket   bucket                                      │
  └──────────────────────────────────────────────────────┘
       │
       ▼
  ┌─────────────────────────────────────────┐
  │ Map<Boolean, List<Employee>>              │
  │   true  -> [Rajesh, Pankaj]   (> 50000)   │
  │   false -> [Priya, Amit]      (<= 50000)  │
  │                                           │
  │   both keys ALWAYS present                │
  └─────────────────────────────────────────┘
DIAGRAM
  groupingBy vs partitioningBy when no element matches:

  groupingBy(isHigh)          partitioningBy(isHigh)
  ┌────────────────┐          ┌────────────────────┐
  │ false -> [...]  │          │ true  -> []  (kept) │
  │ (true MISSING)  │          │ false -> [...]      │
  └────────────────┘          └────────────────────┘

Key Takeaway

Partitioning is grouping specialised for a binary split. When Priya at the National Bank of India must sort staff into those earning above a threshold and everyone else, she uses Collectors.partitioningBy with a predicate, and every element is routed to either the true bucket or the false bucket, returning a Map>. Here the predicate is salary strictly greater than 50,000, so higher earners land under true and everyone at or below 50,000, including exactly 50,000, lands under false.

The difference from groupingBy to always stress is that partitioningBy guarantees both keys are present. With groupingBy a category only appears if some element produced it, so a missing group is simply absent and downstream code may hit a missing key, whereas partitioningBy always returns both true and false, giving an empty list rather than a missing key when a side is empty. That makes it the safer, cleaner choice for a genuinely two-way split, and it is marginally more efficient because it uses a fixed two-slot structure rather than a general hash map.

For money, the predicate must use BigDecimal.compareTo, never == or a conversion to double, because reference equality and floating-point both give wrong answers at the boundary. In this benefits-tier split, higher earners and at-or-below earners were separated, and because both buckets are always present the tier-assignment logic never had to null-check either side.

partitioningBy also accepts a downstream collector just like groupingBy, so partitioningBy(predicate, counting()) gives a quick count on each side for a summary, and partitioningBy(predicate, reducing(...)) gives the total salary on each side.

Remember one sentence: partitioningBy is groupingBy for a true-or-false rule, and it always returns both the true and the false key.

---

Share
Use ← → arrow keys to navigate