Back to Blog
Java

Java Collectors.partitioningBy With Examples

Learn how Java Collectors.partitioningBy splits a stream into two groups by a predicate, with downstream collectors, null handling, and practical tradeoffs.

Java StreamsCollectorsStream APIFunctional ProgrammingData Partitioning
Editorial diagram of a data stream splitting into two groups by a boolean predicate, illustrating Java partitioningBy.

Collectors.partitioningBy is a Java Stream API collector that splits a stream into exactly two groups based on a Predicate. When passed to the terminal collect operation, it returns a Map<Boolean, List<T>>: the true key maps to the elements that match the predicate, and the false key maps to the rest. In contrast to Collectors.groupingBy, which can produce any number of buckets, partitioningBy always produces a two-key map.

The Basic Two-Key Map

The single-argument form takes a Predicate and collects 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 stream traversal. 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.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); Map<Boolean, Long> counts = numbers.stream() .collect(Collectors.partitioningBy( n -> n % 2 == 0, Collectors.counting())); List<String> names = List.of("Alice", "Bob", "Carol", "Dave"); Map<Boolean, Set<String>> groupedNames = names.stream() .collect(Collectors.partitioningBy( name -> name.length() > 4, Collectors.toSet()));

The downstream collector is applied separately to the elements in each partition and produces the 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.

ConcernpartitioningBygroupingBy
Number of groupsAlways twoDepends on data
Empty groups in resultBoth keys always presentOnly keys that appeared
Key typeBooleanClassifier result type
Downstream collectorSupportedSupported

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 main work is one predicate evaluation per element, and each element is stored in one of the two result lists.

With the default downstream collector, 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.

Java Collectors.partitioningBy Examples | RYUSLOG DEV