Collectors.partitioningBy()
Splits a stream into the elements that match a predicate and those that do not, with both groups always present.
public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$predicate | Predicate<? super T> | Decides which side each element goes to. |
$downstreamoptional | Collector<? super T, A, D>= Collectors.toList() | Reduces the elements on each side, such as counting() or mapping(...). |
Return value
Collector<T, ?, Map<Boolean, D>>A collector that produces a Map<Boolean, D> with exactly two keys, false and true. Each value is the downstream result for the elements on that side, or the downstream's empty result when no element matched.
Errors
Building the collector throws NullPointerException for a null downstream; a null predicate is only detected when collect runs, as a NullPointerException. Exceptions thrown by the predicate propagate from collect.
Collectors.partitioningBy takes a predicate and splits the stream into two groups in one pass: the elements for which it returned true and those for which it returned false. The result is a Map<Boolean, List<T>>, or a Map<Boolean, D> when you pass a downstream collector.
Map<Boolean, List<Row>> split = rows.stream()
.collect(Collectors.partitioningBy(Row::isValid));
List<Row> valid = split.get(true);
List<Row> invalid = split.get(false);Why not two filters?
Two filter calls walk the data twice and run the predicate twice per element. That matters when the source can only be read once (a file, a result set, a message batch) or when the predicate is expensive, such as a validation that parses a date. partitioningBy reads each element once and keeps both sides.
Both keys are always there
This is the real difference from groupingBy(x -> condition). With groupingBy, a key exists only if some element produced it, so when nothing fails validation, get(false) returns null. partitioningBy always holds both false and true; an empty side is an empty list, a count of 0 or a sum of 0, depending on the downstream collector. Code that reads get(false) straight after a batch import is safe without a null check.
Downstream collectors
The second argument works as it does for groupingBy: counting() for how many passed and failed, mapping(Row::id, toList()) to keep only ids, summingInt for totals on each side.
Things to know
The keys are Boolean objects, so get(true) autoboxes, which is fine. The map's type and mutability are not specified; treat it as read-only and copy the lists out if you need to change them. When you need three or more groups, such as small, medium and large orders, use groupingBy with an enum key instead of nesting two partitions.
Example
When to use it
- Splitting an uploaded CSV into rows to import and rows to report back as errors.
- Separating verified and unverified accounts to send reminder emails only to the second group.
- Counting how many health checks passed and failed across a set of downstream services.
Frequently asked questions
What is the difference between partitioningBy and groupingBy?
partitioningBy takes a Predicate and always produces exactly two keys, false and true, even if one side is empty. groupingBy takes any key function and only creates keys that occur, so grouping on a boolean can leave a key missing and get() returns null.
How do I split a list into two lists by a condition in Java?
Use list.stream().collect(Collectors.partitioningBy(condition)). The result's get(true) holds the elements that matched and get(false) holds the rest, built in a single pass over the list.
Can partitioningBy return something other than lists?
Yes. Pass a downstream collector as the second argument, such as Collectors.counting() for a count per side or Collectors.mapping(Item::id, Collectors.toList()) to keep only one field.
More examples
Validating an import batch
One pass gives both the rows to save and the rows to report back to the uploader. Because both keys always exist, the error loop needs no null check even when the whole file is valid.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
record Row(int line, String email) {
boolean valid() { return email != null && email.contains("@"); }
}
void main() {
List<Row> rows = List.of(
new Row(1, "[email protected]"), new Row(2, "not-an-email"),
new Row(3, "[email protected]"), new Row(4, null));
Map<Boolean, List<Row>> split = rows.stream()
.collect(Collectors.partitioningBy(Row::valid));
IO.println("importing " + split.get(true).size() + " rows");
for (Row bad : split.get(false)) {
IO.println("line " + bad.line() + ": invalid email " + bad.email());
}
}
// Output:
// importing 2 rows
// line 2: invalid email not-an-email
// line 4: invalid email nullpartitioningBy versus groupingBy on a boolean
With no slow requests, groupingBy has no true key at all and get(true) returns null, which is a NullPointerException waiting to happen in the next line of code. partitioningBy fills the missing side with an empty list.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
void main() {
List<Integer> latenciesMs = List.of(120, 80, 300);
Map<Boolean, List<Integer>> grouped = latenciesMs.stream()
.collect(Collectors.groupingBy(ms -> ms > 1000));
Map<Boolean, List<Integer>> partitioned = latenciesMs.stream()
.collect(Collectors.partitioningBy(ms -> ms > 1000));
IO.println("groupingBy: " + grouped + ", slow = " + grouped.get(true));
IO.println("partitioningBy: " + partitioned + ", slow = " + partitioned.get(true));
}
// Output:
// groupingBy: {false=[120, 80, 300]}, slow = null
// partitioningBy: {false=[120, 80, 300], true=[]}, slow = []Totals on each side
Any downstream collector works, here summingInt for a total per side. The same pattern answers questions such as how much revenue came from orders that qualified for free shipping.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
record Order(String id, int cents) {}
void main() {
List<Order> orders = List.of(
new Order("o-1", 25_000), new Order("o-2", 1_200),
new Order("o-3", 40_000), new Order("o-4", 900));
Map<Boolean, Integer> revenue = orders.stream()
.collect(Collectors.partitioningBy(o -> o.cents() >= 10_000,
Collectors.summingInt(Order::cents)));
IO.println("large orders: " + revenue.get(true) + " cents");
IO.println("small orders: " + revenue.get(false) + " cents");
}
// Output:
// large orders: 65000 cents
// small orders: 2100 cents
Discussion