Java Gold試験の学習を進めていると、「groupingByとpartitioningByの違いがよくわからない」「分割した結果がBooleanキーになるのはわかるけど、ダウンストリームコレクタとの組み合わせ方が曖昧」という悩みにぶつかる方が多いのではないでしょうか。実際、partitioningByは一見シンプルなメソッドですが、試験では「キーが必ず2つ存在する」という仕様を突いた選択肢が頻出します。この記事では、partitioningByの基本動作から応用パターン、そして試験で狙われやすいひっかけポイントまでを整理して解説します。

Collectors.partitioningByとは何か

partitioningByは、Streamの要素をtrue・falseの2グループに分割するためのCollectorです。groupingByが任意の分類キーでグループ化するのに対し、partitioningByは必ずPredicate(条件式)を使って「条件に合うか合わないか」の2択で要素を振り分けます。

戻り値の型はMap<Boolean, List<T>>で、キーは常にtrueとfalseの2つだけです。これがgroupingByとの最も大きな違いであり、試験でも重要なポイントになります。

import java.util.*;
import java.util.stream.*;

public class PartitionExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Map<Boolean, List<Integer>> result = numbers.stream()
                .collect(Collectors.partitioningBy(n -> n % 2 == 0));

        System.out.println(result);
        // 出力: {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
    }
}

このように、偶数か奇数かという条件でリストが2つに分かれ、Map<Boolean, List<Integer>>として返されます。

メソッドのシグネチャと2つのオーバーロード

partitioningByには2種類のオーバーロードがあります。試験ではこの違いを正確に把握しているかが問われます。

オーバーロード戻り値の型用途
partitioningBy(Predicate<? super T> predicate)Map<Boolean, List<T>>単純にtrue/falseでリストに分割する
partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream)Map<Boolean, D>分割した各グループに対して、さらに別のCollectorで集計する

2つ目のオーバーロードでは、downstreamにcounting()やmapping()、joining()などを渡すことで、分割したグループをさらに加工できます。

import java.util.*;
import java.util.stream.*;

public class PartitionExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Map<Boolean, List<Integer>> result = numbers.stream()
                .collect(Collectors.partitioningBy(n -> n % 2 == 0));

        System.out.println(result);
        // 出力: {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
    }
}

ここでは「文字数が3より大きいかどうか」で分割し、それぞれの件数をカウントしています。falseグループ(”ai”, “se”)は2件、trueグループ(”java”, “gold”, “exam”, “study”)は4件です。

試験で狙われる最大のひっかけ:キーは必ず2つ存在する

partitioningByの最大の特徴は、条件に合致する要素が0件であっても、true・falseの両方のキーが必ずMapに存在することです。これはgroupingByとの決定的な違いであり、試験で非常によく出題されます。

import java.util.*;
import java.util.stream.*;

public class EmptyPartitionExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(2, 4, 6, 8);

        Map<Boolean, List<Integer>> result = numbers.stream()
                .collect(Collectors.partitioningBy(n -> n % 2 != 0));

        System.out.println(result);
        // 出力: {false=[2, 4, 6, 8], true=[]}
        // 奇数が1つも存在しないが、trueキー自体は空リストとして残る
    }
}

一方、groupingByで同様の処理を書いた場合は、該当する要素が存在しないキーはMapに登場しません。この違いを表で整理します。

項目partitioningBygroupingBy
分類方法Predicate(true/false)任意の分類関数(classifier)
キーの種類必ずBoolean(true/false)のみ分類関数の戻り値に応じて可変
該当0件時の挙動キー自体は存在し、値が空リストになるそのキー自体がMapに出現しない
戻り値の型Map<Boolean, List<T>>またはMap<Boolean, D>Map<K, List<T>>またはMap<K, D>

この「キーが必ず存在するかどうか」の違いを問う問題では、Mapのサイズを問われたり、containsKeyの結果を予想させたりする形式が多いため、正確に覚えておく必要があります。

groupingByにPredicateを渡した場合との違い

groupingByの分類関数にはPredicateを渡すことも文法上は可能です。Predicateも結局はFunction<T, Boolean>の一種だからです。ただし、これは推奨される使い方ではなく、partitioningByとは挙動が異なります。

import java.util.*;
import java.util.stream.*;

public class GroupingByPredicate {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(2, 4, 6, 8);

        Map<Boolean, List<Integer>> result = numbers.stream()
                .collect(Collectors.groupingBy(n -> n % 2 != 0));

        System.out.println(result);
        // 出力: {false=[2, 4, 6, 8]}
        // trueに該当する要素がないため、trueキー自体が存在しない
    }
}

同じ条件式を使っていても、groupingByでは該当する要素がないキーはMapに含まれません。試験ではこの2つを並べて出力結果を選ばせる問題が出やすいため、「partitioningByは固定2キー、groupingByは可変キー」という対比で覚えておくと混同を防げます。

ネストしたpartitioningByの使い方

partitioningByのdownstreamに、さらにpartitioningByを渡すことで、2段階の分割(ネスト)も可能です。地味ですが、試験ではコードの読み取り問題として出題されることがあります。

import java.util.*;
import java.util.stream.*;

public class NestedPartitioning {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Map<Boolean, Map<Boolean, List<Integer>>> result = numbers.stream()
                .collect(Collectors.partitioningBy(
                        n -> n % 2 == 0,
                        Collectors.partitioningBy(n -> n > 5)));

        System.out.println(result);
        // 出力: {false={false=[1, 3, 5], true=[7, 9]},
        //        true={false=[2, 4], true=[6, 8, 10]}}
    }
}

外側のpartitioningByで「偶数か奇数か」、内側のpartitioningByで「5より大きいか」を判定しており、結果はMap<Boolean, Map<Boolean, List<Integer>>>という入れ子構造になります。読み取りに時間がかかるため、分割条件を1つずつ丁寧に追うことが大切です。

まとめ

partitioningByは、Streamの要素をtrue・falseの2グループに分けるためのCollectorで、戻り値は常にMap<Boolean, List<T>>またはMap<Boolean, D>になります。試験対策として押さえておくべき要点は次の3つです。

第一に、partitioningByは該当する要素が0件であっても、true・falseの両方のキーが必ずMapに存在します。第二に、groupingByは分類関数の結果に応じてキーが可変であり、該当しないキーはMapに出現しません。第三に、downstreamコレクタを組み合わせることで、件数集計やネストした分割など柔軟な処理が可能になります。

この3点を意識してコードを読み解けば、partitioningByに関する出力予測問題に落ち着いて対応できるはずです。