CodeGym /コース /JAVA 25 SELF /高度なコレクター

高度なコレクター

JAVA 25 SELF
レベル 31 , レッスン 4
使用可能

1. はじめに

Stream API でコレクションを扱うとき、単に要素をグループ化するだけでなく、同時に何らかの処理(変換・フィルタリング・別の種類のコレクションに収集)を行いたい場面がよくあります。これを支えるのが Java の ダウンストリーム・コレクター — 各グループやデータの部分集合に対して適用される入れ子のコレクターです。

ダウンストリーム・コレクターとは?

グループ化や分割の結果に対して適用されるコレクターのことです。たとえば groupingBy で学生をコースごとにグループ化し、各グループの中で名前だけを集めたり、優秀者だけを集めたり(例: GPA の閾値を 4.5 とする)できます。

mapping

収集する前にグループ内の要素を変換できます。

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • 学生をコースごとにグループ化します.
  • 各グループでは(オブジェクト全体ではなく)名前だけを収集します。

filtering

グループ内の要素をフィルタリングできます(例: GPA >= 4.5 のみ残す)。

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

各グループで優秀者だけを残します。

flatMapping

グループ内の入れ子になったコレクションを「展開」できます。

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

各著者について、その著者の全書籍のタグを一意に収集します。

partitioningBy と downstream

groupingBy に似ていますが、真偽条件で 2 つのグループに分割します。

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

合格/不合格で学生を分け、各グループでは名前のみを収集します。

2. teeing: 2 つのコレクターで同時に集約する

ストリームに対して複数の集計値(例: 合計と平均、最小値と最大値)を同時に計算したいことがあります。これを可能にするコレクターが Java 12+ で導入された teeing です。

teeing はどのように動作するか?

2 つのコレクターと、それらの結果を結合する関数を渡します。

構文:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

最小値 + 最大値

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

最小値と最大値を同時に求めます。

合計 + 平均

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

合計と平均を持つオブジェクトを得ます。

例: 給与レポート

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

SalaryStats に合計と平均の両方を保持します。

3. toUnmodifiableList/Set/MapcollectingAndThen による「凍結」

近年の Java には、作成後に変更できないコレクション — 不変unmodifiable)コレクションがあります。結果をうっかり変更できないことが重要な API では便利です。

toUnmodifiableList/Set/Map

  • 不変コレクションを返します。
  • 要素の追加/削除を試みると UnsupportedOperationException が発生します。

例:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

コレクターの結果に関数を適用できます。たとえばコレクションを「凍結」します。

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

まず通常のリストに収集し、その後に不変化します。

Set の例:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. パイプラインの実例

レポートとスライス別統計

高度なコレクターを使うと、複雑なレポートや統計を「ワンライナー」で構築できます。

例: 部門別の平均給与

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

例: カテゴリごとの高価な商品トップ3

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

API 契約としての不変な結果

メソッドが変更不可能なコレクションを返すなら、偶発的なミスを防ぎ、API をより堅牢にできます。

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

利用者は getTags().add("新しいタグ") のような操作はできず、例外が発生します。

例: 複数の集計を含むレポート(teeing

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

SalaryReport に平均給与と総給与の両方を保持します。

5. 典型的な落とし穴と注意点

エラー1: 不変性を忘れる。 通常のリストを返すと、後から変更される可能性があります。結果を「凍結」するには toUnmodifiableList/Set/Map または collectingAndThen を使用します。

エラー2: 不適切なダウンストリーム・コレクター。 グループ内の要素を変換したい場合は mapping、フィルタリングしたい場合は filtering、入れ子のコレクションを「展開」したい場合は flatMapping を使います。

エラー3: UnsupportedOperationException toUnmodifiableList/Set/Map で収集したコレクション、または「凍結」した collectingAndThen のコレクションを変更しようとすると発生します。

エラー4: 一意性の欠落/キー衝突。 toUnmodifiableSet では要素が一意である必要があり、toUnmodifiableMap ではキーが一意である必要があります。満たされない場合は収集中に例外が発生します。

1
アンケート/クイズ
グルーピングとアグリゲーション、レベル 31、レッスン 4
使用不可
グルーピングとアグリゲーション
Stream API: グルーピングとアグリゲーション
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION