Collector.of でカスタム Collector 作成 — 特殊な集約
Stream API の Collectors には便利な標準 Collector が多数ありますが、特殊な集約処理をしたいときは Collector.of を使って自作できます。
初心者がつまずきやすい「Collector の仕組み」を、コード例とテンプレートでかみ砕いて説明します。
Collector の基本構造
Collector.of は以下の 3〜4 つの関数を渡して作ります:
Collector.of(
Supplier<A> supplier, // 初期コンテナの生成
BiConsumer<A, T> accumulator, // 要素をコンテナに追加する方法
BinaryOperator<A> combiner, // 並列処理時に部分結果を結合する方法
Function<A, R> finisher // 最終結果への変換(省略可)
);
Java- supplier: 集約のための「入れ物」を作る。例:
ArrayList::new - accumulator: 要素をどう入れるか。例:
(list, e) -> list.add(e) - combiner: 並列処理で部分結果をどうまとめるか。例:
(l1, l2) -> { l1.addAll(l2); return l1; } - finisher: 最終的に結果をどう変換するか。省略すると「入れ物そのまま」が返る。
基本コード例
1) リストに集約(標準 toList と同じ)
Collector<String, List<String>, List<String>> toListCustom =
Collector.of(
ArrayList::new, // supplier
List::add, // accumulator
(l1, l2) -> { l1.addAll(l2); return l1; }, // combiner
Function.identity() // finisher
);
List<String> result = Stream.of("a","b","c").collect(toListCustom);
System.out.println(result); // [a, b, c]
Java2) 文字列を連結(区切り付き)
Collector<String, StringBuilder, String> joiningWithComma =
Collector.of(
StringBuilder::new,
(sb, s) -> {
if (sb.length() > 0) sb.append(", ");
sb.append(s);
},
(sb1, sb2) -> {
if (sb1.length() == 0) return sb2;
if (sb2.length() > 0) sb1.append(", ").append(sb2);
return sb1;
},
StringBuilder::toString
);
String joined = Stream.of("apple","banana","cherry").collect(joiningWithComma);
System.out.println(joined); // apple, banana, cherry
Java3) 合計と件数を同時に計算(平均値を出す)
record Stats(int sum, int count) {}
Collector<Integer, Stats, Double> avgCollector =
Collector.of(
() -> new Stats(0,0),
(st, n) -> { st = new Stats(st.sum()+n, st.count()+1); },
(s1, s2) -> new Stats(s1.sum()+s2.sum(), s1.count()+s2.count()),
st -> st.count() == 0 ? 0.0 : (double) st.sum()/st.count()
);
Java※ 上記はイミュータブル record を使った例。可変クラスならフィールド更新で書けます。
例題で理解する
例題1: ユニークな要素を保持する Collector
Collector<String, Set<String>, Set<String>> toUniqueSet =
Collector.of(
HashSet::new,
Set::add,
(s1, s2) -> { s1.addAll(s2); return s1; },
Function.identity()
);
Set<String> uniq = Stream.of("a","b","a","c").collect(toUniqueSet);
System.out.println(uniq); // [a, b, c]
Java例題2: 文字数合計を Collector で作る
Collector<String, int[], Integer> lengthSum =
Collector.of(
() -> new int[1], // supplier
(acc, s) -> acc[0] += s.length(),// accumulator
(a1, a2) -> { a1[0] += a2[0]; return a1; }, // combiner
acc -> acc[0] // finisher
);
int totalLen = Stream.of("Java","Stream","Collector").collect(lengthSum);
System.out.println(totalLen); // 18
Javaテンプレート集
- リスト化
Collector<T, List<T>, List<T>> toListCustom =
Collector.of(ArrayList::new, List::add,
(l1,l2)->{ l1.addAll(l2); return l1; },
Function.identity());
Java- 文字列連結
Collector<String, StringBuilder, String> joining =
Collector.of(StringBuilder::new,
(sb,s)->{ if(sb.length()>0) sb.append(","); sb.append(s); },
(sb1,sb2)->{ sb1.append(",").append(sb2); return sb1; },
StringBuilder::toString);
Java- 数値集計(合計)
Collector<Integer, int[], Integer> sumCollector =
Collector.of(() -> new int[1],
(acc,n)->acc[0]+=n,
(a1,a2)->{a1[0]+=a2[0]; return a1;},
acc->acc[0]);
Java落とし穴と回避策
- supplier/accumulator/combiner の役割混同: supplier は「新しい入れ物」、accumulator は「要素追加」、combiner は「並列時の結合」。役割を明確に。
- 可変 vs 不変: 可変オブジェクト(ArrayList, StringBuilder)ならフィールド更新で簡単。不変なら新しいオブジェクトを返す必要がある。
- 並列処理: combiner が正しくないと並列ストリームで誤動作。必ず「部分結果を正しく結合」できるようにする。
- finisher の省略: 結果が「入れ物そのまま」で良いなら
Function.identity()を指定。
まとめ
Collector.ofは「特殊な集約」を自作するための仕組み。- supplier / accumulator / combiner / finisher の4つを理解すれば、どんな集約も作れる。
- 標準 Collector にない「特殊な集計・変換」を Collector.of で安全に表現できる。
👉 練習課題: 「文字列リストから、文字数合計と平均を同時に計算する Collector」を作ってみると、Collector.of の仕組みが直感で理解できます。
