1. mutableなCollectorの危険性とレビュー観点

JavaのCollectorは、内部的に可変構造(mutable container)を使用して状態を蓄積するため、設計次第ではスレッドセーフでない構造になります。
これはとくにparallelStream()時に意図しない動作や競合を引き起こす原因になります。

1.1 典型的な危険構造

Collector<String, List<String>, List<String>> unsafeCollector = Collector.of(
    ArrayList::new,
    List::add,
    (l1, l2) -> { l1.addAll(l2); return l1; }
);

この構造は並列Streamで使うと、l1.addAll(l2)のタイミングで競合・破壊的合成が発生しうる構造です。

mutable構造でのcombinerは、“返すオブジェクトを破壊的に変更する”構造を避けるのが鉄則。

レビュー観点

  • combinerが破壊的変更(in-place変更)になっていないか?
  • List.addAll()Map.putAll()で戻り値をそのまま再利用していないか?
  • 並列Streamを想定した設計かどうか?

2. カスタムCollectorを並列対応させる設計ポイント

スレッドセーフなCollectorを自作する場合は、特にcombinerの実装が重要になります。
並列処理時に蓄積用の構造を共有せず、安全に結合できる設計が求められます。

2.1 安全な構造例(Immutable構造を再生成)

Collector<String, List<String>, List<String>> safeCollector = Collector.of(
    ArrayList::new,
    List::add,
    (left, right) -> {
        List<String> merged = new ArrayList<>(left);
        merged.addAll(right);
        return merged;
    }
);

このように、結合用の構造は必ず新しく生成することで、既存インスタンスの破壊を防ぎます。

mutableな蓄積構造を使う場合でも、“combinerでは必ず新しいインスタンスに結果を移す”ことが原則。

レビュー観点

  • combinerで副作用が発生していないか?
  • finisherで不変構造に変換すべきかどうか?
  • 並列性を担保できる構造(immutable化 or thread-local構造)になっているか?

3. teeing()の複合集約構造と可読性のトレードオフ

Java 12以降で導入されたCollectors.teeing()は、2つのCollectorを並行実行し、それぞれの結果を1つの出力に合成できる構造です。
これは柔軟性の高い構造ですが、読解難易度が高くなりがちなため、レビューでは「意図が明確に読み取れるかどうか」が焦点になります。

3.1 使用例:合計と平均の同時取得

Collector<Integer, ?, Summary> summaryCollector = Collectors.teeing(
    Collectors.summingInt(x -> x),
    Collectors.averagingInt(x -> x),
    Summary::new
);

ここでSummary(sum, avg) を受け取るDTOです。

teeingは「データの2分割集約」ではなく、「同じstreamの2方向集約」を可能にする構造。

利点

  • streamの評価を1回で済ませつつ、複数の異なる集約を実現できる

留意点

  • 構造が入れ子になりやすく、レビューで意図が読みづらい
  • 合成後の構造(DTOやPair)の定義が曖昧だと、「何が返るのか」が不明瞭

レビュー観点

  • teeingで作成された合成構造は名前付きのDTOなどで明示されているか?
  • “合成結果の意味”が型や変数名から読み取れる構造になっているか?
  • 必要に応じて、合成先構造(Tuple/Pairsなど)を明示的に型定義しているか?

4. filtering()の設計的使いどころと責務分離

Java 9 で導入された Collectors.filtering() は、groupingBy などの downstream collector の前段階で条件を適用できる構造です。
従来は filter().collect(...) で対応していたが、groupingBy などと併用する場合に責務分離として明確になります。

4.1 通常の構造との比較

従来の例:

users.stream()
  .filter(User::isActive)
  .collect(Collectors.groupingBy(User::getDepartment));

filtering()を使った構造:

users.stream().collect(Collectors.groupingBy(
  User::getDepartment,
  Collectors.filtering(User::isActive, Collectors.toList())
));

filtering は「特定のグループにおける条件付き収集」を構造の中で表現できるため、
stream 全体の filter に比べて 局所的な目的が明確に見える

レビュー観点

  • filteringは必要な粒度(group内)に対して使われているか?
  • 単純なstream filterとの責務の違いが意識されているか?
  • groupingByとの組み合わせで意図が過剰に複雑化していないか?

5. flatMapping()によるStreamネストの解消と再構築

Java 9 で導入された flatMapping() は、ネストされた構造(List<Set> など)を flatten する用途に特化した Collector 拡張です。

5.1 基本形

Map<String, Set<String>> deptToTags =
  users.stream().collect(Collectors.groupingBy(
    User::getDepartment,
    Collectors.flatMapping(
      user -> user.getTags().stream(),
      Collectors.toSet()
    )
  ));

これは、「部署ごとの全タグ集合」を求める典型例です。

flatMapping によって、ネスト構造の flatten と mapping が一度に実現できる。
複雑な map(...).flatMap(...).collect(...) を避けられる。

レビュー観点

  • flatMapping の使用が「List の List」や「Set の Set」などのネスト構造に対して正当化されているか?
  • mapping 関数の戻り値が null を返す可能性がないか?(→ 空streamで防御されているか?)
  • 返却される構造が Set / List / Optional など、明示的な意図を持って選定されているか?

6. Collector設計における柔軟性と安全性のバランス戦略

これまでの章で見たように、Collectorは非常に表現力が高い一方で、過剰な合成・副作用の混入・スレッド安全性の欠如といった罠も併せ持ちます。

6.1 柔軟な構造設計の要点

  • downstream collector は目的ごとに明確に分ける
  • filtering/flatMapping のような“構造変換の粒度”を使い分ける
  • 合成された Collector は必ず 命名された関数DTO で意味づけを補う

6.2 安全性を担保するポイント

  • mutable構造は combiner で破壊的操作を行わない
  • collectingAndThen で外部サービスを呼ばない(副作用禁止)
  • teeing などの複合集約では意味づけ構造を明示(recordPair<T1,T2>

Collectorは「コードを短くする道具」ではなく、「構造を組み立てる設計部品」。
レビューアーは、そのCollectorが「何を表していて、どう合成されているか」を読み解く力が問われる。

総まとめ:Collectorを“コード”としてではなく“構造”として読む

Stream API における Collector は、設計における「出口のかたち」を決定づける構成要素です。
以下の観点を踏まえることで、Collector は「動く処理」から「意味を持つ構造」へと進化します。

✔ Collector設計のチェックポイント

観点 チェック内容
スレッド安全性 combinerは破壊的でないか?
複合集約 teeingの出力構造に名前や意味があるか?
ネスト解消 flatMappingは適切な場所で使われているか?
条件付き集約 filteringでロジックをstream外に追い出せるか?
構造可読性 collect文の意図が関数名・DTOで補足されているか?
UML Diagram