Java Collectors PartitioningBy With Examples
java collectors partitioningby: Use Java Collectors.partitioningBy to split streams into two groups by a predicate, with downstream collectors and practical tradeoffs...
java collectors partitioningby requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Java, Collectors.partitioningBy is the Stream API method for splitting a stream into exactly two groups based on a predicate. Where groupingBy can produce any number of buckets, partitioningBy produces a Map<Boolean, List<T>> with two keys: true for elements that match the predicate and false for the rest. When used with the terminal collect operation, it consumes the stream and returns the completed map.
The Basic Two-Key Map
The single-argument form takes a Predicate and returns a collector that accumulates matching and non-matching elements into separate lists.
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); Map<Boolean, List<Integer>> evenAndOdd = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); List<Integer> evens = evenAndOdd.get(true); // [2, 4, 6] List<Integer> odds = evenAndOdd.get(false); // [1, 3, 5]
The predicate is evaluated once per element during a single traversal of the stream. In the standard OpenJDK implementation the resulting lists are mutable ArrayList instances, so you can modify a partition after collection if your use case requires it.
A detail that surprises developers coming from groupingBy is that both keys are always present in the returned map. If every element matches the predicate, the false list is empty but still exists. That makes get(true) and get(false) safe without null checks, which is one of the main reasons to prefer partitioningBy over a two-key groupingBy call.
Partitioning With a Downstream Collector
The two-argument overload replaces the default list with any downstream collector. This is where partitioningBy becomes more than a convenience for splitting lists.
Map<Boolean, Long> counts = numbers.stream() .collect(Collectors.partitioningBy( n -> n % 2 == 0, Collectors.counting())); Map<Boolean, Set<String>> groupedNames = names.stream() .collect(Collectors.partitioningBy( name -> name.length() > 4, Collectors.toSet()));
The downstream collector receives the elements that fall into each partition and produces the final value for that key. Common choices are counting(), toSet(), mapping(), summingInt(), and even another partitioningBy for nested splits. The map value type changes from List<T> to whatever the downstream collector returns, so Map<Boolean, Long> and Map<Boolean, Set<String>> are both valid results.
Partitioning Versus groupingBy
Both methods partition elements, but they answer different questions. groupingBy assigns each element to a key produced by a classifier function, so the number of groups depends on the input data. partitioningBy is a special case where the classifier is a predicate and the key space is fixed to two booleans.
| Concern | partitioningBy | groupingBy |
|---|---|---|
| Number of groups | Always two | Depends on data |
| Empty groups in result | Both keys always present | Only keys that appeared |
| Key type | Boolean | Classifier result type |
| Downstream collector | Supported | Supported |
Use partitioningBy when the decision is genuinely binary: valid or invalid, present or missing, over threshold or under. Use groupingBy when the categories are open-ended, such as grouping orders by month or users by role. If you find yourself calling groupingBy with a classifier that only ever returns two values, partitioningBy is the more precise tool because it guarantees both keys exist.
Null Elements and Predicate Behavior
A null element is not special-cased by partitioningBy. The predicate is invoked on it directly, so predicate.test(null) determines the partition. A predicate like n -> n % 2 == 0 throws NullPointerException on a null element, just as it would anywhere else in the stream pipeline.
If nulls are possible, handle them inside the predicate:
Map<Boolean, List<String>> partitioned = values.stream() .collect(Collectors.partitioningBy( s -> s != null && s.startsWith("http")));
The element null lands in the false partition because the predicate short-circuits on the null check. This keeps the pipeline from failing while still separating nulls from valid matches.
Runtime Cost and Memory Behavior
partitioningBy is an eager terminal operation. It consumes the entire stream and builds the result map in memory before returning, so it is not suitable for infinite streams. The cost is a single traversal with one predicate evaluation per element, plus the storage for every element in one of the two lists.
The two lists together hold all elements from the stream, so memory usage is proportional to the input size. If you only need a count or a sum per partition, the downstream collector form avoids materializing the full lists and keeps only the accumulated result. That distinction matters when the stream is large and the elements are heavy objects.
The collector also works with parallel streams, and the framework merges partial results from each thread. A stateless predicate avoids any concern about shared state during parallel execution.
A Practical Example: Partitioning Validation Results
A common production use is separating valid records from rejected ones in a single pass over a batch of input.
record Order(int id, BigDecimal total, String status) {} List<Order> orders = loadOrders(); Map<Boolean, List<Order>> validAndInvalid = orders.stream() .collect(Collectors.partitioningBy( order -> order.total().compareTo(BigDecimal.ZERO) > 0 && "ACTIVE".equals(order.status()))); List<Order> valid = validAndInvalid.get(true); List<Order> invalid = validAndInvalid.get(false);
Because both keys are guaranteed to exist, the caller can process valid and invalid without defensive null checks. If the validation logic later grows beyond a boolean outcome, such as distinguishing rejected, pending, and approved, that is the signal to switch from partitioningBy to groupingBy with an enum classifier.