Back to Stream: Grouping & Partitioning
Q16Question 16 of 22

Partition employees into high and low earners and count each partition using partitioningBy with a downstream collector

Key Concepts
  • partitioningBy(predicate) always returns exactly two keys, true and false.
  • The two-argument form applies a downstream collector to each partition in one pass.
  • partitioningBy(pred, counting()) gives Map<Boolean, Long>, the count on each side.
  • Both keys are always present even when one partition is empty, unlike groupingBy.
  • groupingBy can produce any number of keys and only creates a key when an element maps to it.
  • Use BigDecimal.compareTo for the money threshold, never == or a double.
  • Guaranteed both-keys means downstream reads need no null handling.

Definition

`partitioningBy(pred, counting())` yields two keys, true and false, each mapped to the count of elements on that side, in a single pass.

Collectors.partitioningBy splits a stream into exactly two groups and can reduce each side.

  • partitioningBy(predicate) – returns Map> keyed by true and false.
  • Two-argument form – partitioningBy(predicate, downstream) applies a downstream collector to each partition.
  • Counting each side – partitioningBy(pred, counting()) gives Map.
  • Both keys guaranteed – even an empty partition keeps its key, which groupingBy does not promise.
  • One pass – the count is computed while partitioning, not in a second traversal.

Rajesh at the National Bank of India sorts salary slips into just two trays: above ten lakh and not above ten lakh. partitioningBy always makes exactly these two trays, even if one ends up empty, and he counts the slips in each tray as he sorts them.

Understanding the Concepts

What is partitioningBy with a downstream collector?

partitioningBy(predicate) is a specialization of grouping where the classifier is a boolean predicate, so the result map always has exactly two keys, true and false. The two-argument form partitioningBy(predicate, downstream) applies a downstream collector to each partition.

JAVA
package com.bank.compliance;

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

public class EarnerCensus {
    private static final BigDecimal TEN_LAKH = new BigDecimal("1000000");

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

Why does partitioningBy always have exactly two keys?

The classifier is a boolean, and a boolean has only two possible values. partitioningBy pre-creates both the true and false entries, so even if no element falls on one side, that key still exists with the downstream's identity value, like an empty list or a count of zero.

JAVA
// Even if nobody earns above ten lakh, true is present with count 0.
Map<Boolean, Long> counts = List.of(
                new Employee("Amit", new BigDecimal("400000")))
        .stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(new BigDecimal("1000000")) > 0,
                Collectors.counting()));
System.out.println(counts); // {false=1, true=0}

How does the count happen in one pass?

The downstream counting() collector maintains a running count per partition. As partitioningBy routes each element to its side, it hands the element to that side's counting() accumulator, which increments. There is no second traversal: both the routing and the counting happen together.

JAVA
package com.bank.compliance;

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; }
}

How do you run a full example?

JAVA
package com.bank.compliance;

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

public class EarnerCensusDemo {
    private static final BigDecimal TEN_LAKH = new BigDecimal("1000000");

    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee("Rajesh", new BigDecimal("1200000")),
                new Employee("Priya", new BigDecimal("1500000")),
                new Employee("Amit", new BigDecimal("900000")),
                new Employee("Pankaj", new BigDecimal("400000")));

        Map<Boolean, Long> counts = employees.stream()
                .collect(Collectors.partitioningBy(
                        e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                        Collectors.counting()));

        System.out.println("High earners (> 10 lakh): " + counts.get(true));
        System.out.println("Others (<= 10 lakh): " + counts.get(false));
    }
}
BASH
javac com/bank/compliance/Employee.java com/bank/compliance/EarnerCensusDemo.java
java com.bank.compliance.EarnerCensusDemo
TEXT
High earners (> 10 lakh): 2
Others (<= 10 lakh): 2

How is partitioningBy different from groupingBy?

groupingBy can produce any number of keys and only creates a key when at least one element maps to it, while partitioningBy is strictly two-valued and always includes both keys.

| Aspect | groupingBy | partitioningBy |
|--------|-----------|----------------|
| Classifier | any function | a boolean predicate |
| Key count | any, data driven | exactly two |
| Empty side | key absent | key present, empty value |
| Downstream support | yes | yes |
| Key type | any K | Boolean |

JAVA
// groupingBy on a boolean: the true key is missing when nobody qualifies.
Map<Boolean, Long> grouped = employees.stream()
        .collect(Collectors.groupingBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));
// may be just {false=4}

// partitioningBy: always {false=..., true=...}.
Map<Boolean, Long> partitioned = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));

What downstream collectors can you use?

The downstream can be any collector, not only counting(). Use toList for the members, averagingDouble for a mean, mapping to store derived values, or reducing with BigDecimal to total money per side.

JAVA
// Count per side.
Map<Boolean, Long> countSides = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));

// Names per side.
Map<Boolean, java.util.List<String>> namesPerSide = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.mapping(Employee::getName, Collectors.toList())));

What are common mistakes?

The money mistake is comparing BigDecimal with == or via double. A structural mistake is replacing partitioningBy with groupingBy and then crashing when the expected key is missing on an empty side.

JAVA
// WRONG: == compares references; double loses precision.
// e -> e.getSalary() == TEN_LAKH
// e -> e.getSalary().doubleValue() > 1_000_000.0

// CORRECT: compareTo for exact money comparison.
Map<Boolean, Long> correct = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));

What are the edge cases?

An empty input stream still returns {false=0, true=0} with counting. If every element satisfies the predicate, the false key maps to a count of zero rather than disappearing. The boundary value depends on > versus >=.

JAVA
// Edge case: empty input yields both keys with zero counts.
Map<Boolean, Long> emptyCounts = List.<Employee>of().stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));
System.out.println(emptyCounts); // {false=0, true=0}

When should you use partitioningBy with counting, and when not?

Use it when a true-or-false business rule must be summarized with a count, like a compliance report of high earners versus everyone else. Do not use it for more than two bands, where groupingBy on an enum is correct, and do not use it merely to count the whole stream, where count() is simpler.

JAVA
// Use partitioningBy + counting: binary rule with a summary count.
Map<Boolean, Long> summary = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.counting()));

// Do NOT: three salary bands need groupingBy on an enum classifier.

What are common interview follow-up questions?

Interviewers ask why partitioning guarantees both keys, how the downstream is applied per partition, how it differs from grouping on a boolean, why BigDecimal.compareTo is needed, and how to total money per side. A senior answer notes partitioningBy is slightly more efficient than grouping by a boxed Boolean because it uses a fixed two-slot structure.

JAVA
// Follow-up: count and total money per side together using teeing (Java 12+).
import java.math.BigDecimal;

record Band(long count, BigDecimal total) {}

Map<Boolean, Band> bandSummary = employees.stream()
        .collect(Collectors.partitioningBy(
                e -> e.getSalary().compareTo(TEN_LAKH) > 0,
                Collectors.teeing(
                        Collectors.counting(),
                        Collectors.reducing(BigDecimal.ZERO,
                                Employee::getSalary, BigDecimal::add),
                        Band::new)));

Visual Overview

DIAGRAM
  List<Employee>
       │
       │  partitioningBy(salary > 10 lakh, counting())
       ▼
  ┌──────────── for each element ─────────────┐
  │   predicate true?                           │
  │      ┌──────┴──────┐                         │
  │      ▼             ▼                         │
  │  true side      false side                  │
  │  count++         count++                     │
  └─────────────────────────────────────────────┘
       │
       ▼
  ┌─────────────────────────────┐
  │ Map<Boolean, Long>            │
  │   true  -> 2   (high earners) │
  │   false -> 2   (others)       │
  │   both keys ALWAYS present    │
  └─────────────────────────────┘
DIAGRAM
  groupingBy vs partitioningBy on an empty true side:

  groupingBy(isHigh, counting())   partitioningBy(isHigh, counting())
  ┌───────────────────┐            ┌───────────────────────┐
  │ false -> 4         │            │ true  -> 0  (present)  │
  │ (true MISSING)     │            │ false -> 4            │
  └───────────────────┘            └───────────────────────┘

Key Takeaway

partitioningBy is a specialized grouping where the classifier is a boolean predicate, so the result map always has exactly two keys: true for the elements that satisfy the predicate and false for those that do not. The important guarantee is that both keys are always present, even if one partition is empty, which groupingBy does not promise. The two-argument form adds a downstream collector applied to each side, so with counting() Rajesh gets the size of each partition in a single pass.

At the National Bank of India a compliance report split employees into high earners, who needed enhanced due diligence, and everyone else, and it had to show the count on each side. partitioningBy on the salary predicate with a counting downstream produced a Map, and because both keys are guaranteed the true and false counts could be read directly without worrying about a missing row when a side happened to be empty.

The predicate uses BigDecimal.compareTo against the ten-lakh threshold, never == or a double conversion, because reference equality and floating-point both give wrong answers for money. The downstream is free: swap counting for mapping to list names, for averagingDouble to get a mean, or for a BigDecimal reducing to total money per side.

The senior point to always make is to choose partitioningBy over groupingBy specifically when the split is a true-or-false business rule. groupingBy can produce any number of keys and only creates a key when at least one element maps to it, so you risk a missing key, whereas partitioningBy is strictly two-valued with both keys present. That both-keys guarantee removes null handling when reading the result and is slightly more efficient than grouping by a boxed Boolean.

Remember one sentence: partitioningBy always returns both the true and false key, and its downstream collector reduces each side, so counting each partition is a single pass.

---

Share
Use ← → arrow keys to navigate