Hook: Think of it like a mailroom with exactly two bins: yes and no. Interviewers love this question because it checks whether you know the Stream API shortcut for splitting data in one pass instead of writing two separate filters.
Question: What does Collectors.partitioningBy() do in Java 8?
Answer: partitioningBy takes a Predicate (a true/false test) and splits stream elements into two buckets: one for true and one for false. By default it returns a Map<Boolean, List<T>>, and there is an overload that lets you collect each side into something else, like counts or summaries.
Interview-Ready Answer: In Java 8, Collectors.partitioningBy() is a specialized collector for splitting a stream into two groups based on a boolean test. I use it when I want both sides of the decision in one pass, like active versus inactive users or high-value versus normal orders. The default form returns a Map<Boolean, List<T>>, and the overload with a downstream collector lets me do things like counts or averages without writing two separate stream pipelines.
partitioningBy is a Collector factory in the Java 8 Stream API. A Collector is a recipe that tells a stream how to accumulate results. This one is special because it always splits data into exactly two sides: the items that match the predicate and the items that do not.
The simplest form is effectively partitioningBy(predicate, toList()). The second form accepts a downstream collector, which means the collector used inside each bucket. For example, you can count items, average salaries, or join names instead of keeping a raw list.
true or false.true and one for false. With the default form, each bucket behaves like a list.true, the element goes to the true bucket; otherwise it goes to the false bucket. If you supplied a downstream collector, that collector handles the accumulation inside the bucket.Map<Boolean, ...>.The important mental model is: one pass, two bins. That is why it is often cleaner than writing two separate stream pipelines.
Use partitioningBy when your business rule is naturally binary: pass/fail, paid/unpaid, enabled/disabled, risky/safe. It is especially nice in report code because you often want both sides at once. If you only care about the matching side, plain filter() is simpler.
It also shines when you want a guaranteed shape. The result always has both true and false entries, even if one side is empty. That is useful when UI code or metrics code expects both buckets to exist.
| Tool | Result | Best use | Gotcha |
|---|---|---|---|
partitioningBy | Two buckets | Binary split | Only true/false |
groupingBy(Boolean) | Grouped map | Similar split | May omit a missing key |
filter() | One side | Keep matches only | You lose the false side |
groupingBy(Boolean) can look similar, but partitioningBy is more explicit and guarantees exactly two partitions. That guarantee is often what interviewers are testing.
Time complexity is O(n) because each element is processed once. Space complexity is O(n) if you keep lists, because all elements are stored in one of the two buckets. If your downstream collector is counting() or averagingInt(), the space cost is much smaller because you keep only summary state instead of full lists.
Real number example: on 1 million rows, partitioning does 1 million predicate checks, not 2 million. That is the practical win over two separate passes. The trade-off is that you still hold both groups in memory if you collect to lists.
Edge cases to remember:
NullPointerException.groupingBy instead.Real-World Story: In a checkout service, you might partition orders into fraudReview and clearToShip. The fraud team wants the risky orders, while operations wants the safe ones, and partitioningBy gives both lists in one pass.
What goes wrong when someone misunderstands it? A developer replaces partitioningBy with groupingBy(order -> order.isHighRisk()) and then assumes both keys are always present. On a calm day with no risky orders, the true bucket is missing, so map.get(true) returns null and the dashboard crashes with a NullPointerException. The symptom is a broken report, missing counts in logs, and support tickets from users who suddenly see blank fraud metrics.
Why this matters: In production, the difference between “maybe present” and “always present” is a real outage waiting to happen. partitioningBy is often chosen not just for convenience, but because it makes the result shape predictable.
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class PartitioningByDemo {
static class Employee {
private final String name;
private final int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
int getSalary() {
return salary;
}
@Override
public String toString() {
return name + "(" + salary + ")";
}
}
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Ava", 120000),
new Employee("Ben", 90000),
new Employee("Chloe", 150000),
new Employee("Dan", 50000),
null
);
// Safe predicate: nulls are treated as the false bucket.
// This is the default shape of partitioningBy: two buckets, one true and one false.
Map<Boolean, List<Employee>> buckets = employees.stream()
.collect(Collectors.partitioningBy(e -> e != null && e.getSalary() >= 100000));
System.out.println("High earners: " + buckets.get(true));
System.out.println("Others: " + buckets.get(false));
// Downstream collector example: count how many people fall on each side.
// This avoids storing full lists when we only need summary numbers.
Map<Boolean, Long> counts = employees.stream()
.collect(Collectors.partitioningBy(e -> e != null && e.getSalary() >= 100000,
Collectors.counting()));
System.out.println("Counts: " + counts);
// Empty stream edge case: both keys still exist, even though both buckets are empty.
Map<Boolean, List<Employee>> empty = Collections.<Employee>emptyList().stream()
.collect(Collectors.partitioningBy(e -> e != null && e.getSalary() >= 100000));
System.out.println("Empty map has true? " + empty.containsKey(true));
System.out.println("Empty map has false? " + empty.containsKey(false));
System.out.println("Empty buckets: " + empty);
// Failure path: forgetting to guard against null makes the predicate blow up.
// In real code, this is why null-safe predicates or pre-filtering matter.
try {
employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() >= 100000));
} catch (NullPointerException ex) {
System.out.println("Unsafe predicate failed on null element: " + ex.getClass().getSimpleName());
}
}
}Follow-up & Tricky Questions:
partitioningBy different from groupingBy(Boolean)? partitioningBy is specialized for exactly two buckets and guarantees both keys exist. groupingBy(Boolean) is more general, but a missing bucket may be absent unless you create it yourself.partitioningBy(predicate, downstream) lets each side collect into counts, sums, averages, sets, or joined strings instead of raw lists.toList(). The map itself should not be treated as an ordered structure.true and false entries. With the default collector they are empty lists; with counting() they are zero-like counts.partitioningBy remove the false side? No. It keeps both sides, which is one of its biggest advantages when a caller expects a complete split.groupingBy with a classifier function.partitioningBy faster than two filters? Usually yes, because it scans the data once and tests each element once. Two separate filters mean two passes and often twice the work.null predicate? No. Passing null for the predicate is a programming error and results in a failure.Common Mistakes:
groupingBy(Boolean) when you really need two guaranteed buckets. Correction: use partitioningBy for a binary split.null first or filter bad records before collecting.partitioningBy always keeps both the true and false sides.groupingBy for more.Memory Hook: Picture a mailroom with two trays labeled true and false. Every item gets dropped into exactly one tray, and both trays are handed back at the end.
Cheat Sheet:
Predicate into two buckets.Map<Boolean, List<T>>.partitioningBy(predicate, downstream).O(n) time.Practice Tasks:
counting() instead of lists.