collect を一言でいうと
collect は、
「Stream に流れてきた要素たちを、“コンテナ(List / Set / Map など)や集計結果”にまとめ上げるための終端操作」です。
reduce が「1 つの値」に畳み込むのに対して、collect は「器を用意して、そこにどんどん詰めていく」イメージです。
stream.toList() のような便利メソッドの裏側で、実は collect が動いています。
collect の基本形と Collector という仕組み
collect のシグネチャ
代表的なシグネチャはこれです。
<R, A> R collect(Collector<? super T, A, R> collector)
Javaここで出てくる Collector が、
「どうやって集めるか」を定義した“レシピ”です。
Collector<T, A, R> はざっくり言うと、
- T … Stream の要素の型
- A … 集約の途中で使う“バケツ”(中間バッファ)の型
- R … 最終的な結果の型
を表します。
Collectors.toList() や Collectors.toSet() は、
「List に集めるレシピ」「Set に集めるレシピ」を返しているだけです。
一番よく使う collect:toList / toSet
toList の例
import java.util.List;
import java.util.stream.Collectors;
public class CollectToList {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> upper =
names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper); // [ALICE, BOB, CHARLIE]
}
}
Javaここで起きていることは、
- 空の List を用意する
- Stream の要素を 1 つずつその List に add していく
- 最後にその List を返す
という処理です。
この「空の List を作る」「要素を add する」「部分結果をマージする」という手順を、Collector が中に全部持っていて、collect がそれを呼び出しています。
toSet の例
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectToSet {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Alice");
Set<String> unique =
names.stream()
.collect(Collectors.toSet());
System.out.println(unique); // [Alice, Bob] など(順序は保証されない)
}
}
Javaここでは、「重複を許さない Set に集めるレシピ」を使っています。
Collector の中身をイメージで理解する
supplier / accumulator / combiner
Collector の中身は少し難しく見えますが、
本質的には次の 3 つ(+おまけ)だけです。
「バケツをどう作るか」
「バケツにどう要素を入れるか」
「バケツ同士をどうくっつけるか」
これがそれぞれ、
- supplier … 空のバケツ(中間バッファ)を作る関数
- accumulator … バケツに 1 要素ずつ追加する関数
- combiner … 並列処理時に、バケツ同士をマージする関数
です。
Collectors.toList() を自分で書くとしたら、イメージとしてはこんな感じです。
Collector<T, List<T>, List<T>> toListLike = new Collector<>() {
public Supplier<List<T>> supplier() {
return ArrayList::new; // 空の ArrayList を作る
}
public BiConsumer<List<T>, T> accumulator() {
return List::add; // List に要素を追加する
}
public BinaryOperator<List<T>> combiner() {
return (left, right) -> { left.addAll(right); return left; };
}
public Function<List<T>, List<T>> finisher() {
return Function.identity(); // そのまま返す
}
public Set<Characteristics> characteristics() {
return Set.of(Characteristics.IDENTITY_FINISH);
}
};
Java実際にはもっと汎用的に書かれていますが、
「空の器を作る」「そこに詰める」「器同士をくっつける」
という 3 ステップだと理解しておけば十分です。
文字列連結:joining の例
joining で「区切り付き連結」をする
Collectors.joining は、
「文字列を区切り文字で連結する Collector」です。
import java.util.List;
import java.util.stream.Collectors;
public class CollectJoining {
public static void main(String[] args) {
List<String> words = List.of("Java", "Stream", "collect");
String joined =
words.stream()
.collect(Collectors.joining(", "));
System.out.println(joined); // "Java, Stream, collect"
}
}
Javaここでも、内部では
- 空の
StringBuilderを作る - 各要素を append していく(必要なら区切り文字も)
- 最後に
toString()で文字列にする
という「バケツに詰める」処理が行われています。
グルーピング:groupingBy の例
条件ごとにグループ分けして Map に集める
Collectors.groupingBy は、
「キーを決める関数」を渡して、要素をグループ分けする Collector です。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class User {
private final String name;
private final int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
int getAge() { return age; }
String getName() { return name; }
}
public class CollectGroupingBy {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 17),
new User("Charlie", 20)
);
Map<Integer, List<User>> byAge =
users.stream()
.collect(Collectors.groupingBy(User::getAge));
System.out.println(byAge.get(20)); // [Alice, Charlie]
System.out.println(byAge.get(17)); // [Bob]
}
}
Javaここでは、
- バケツは
Map<Integer, List<User>> - キーは
User::getAge - 各キーごとに List に詰めていく
というレシピになっています。
groupingBy のような「複雑な集約」も、
本質的には「バケツをどう作って、どう詰めるか」を Collector が定義しているだけです。
collect と reduce の違いを整理する
「値 1 つ」か「コンテナ」か
reduce も collect も、
「Stream を 1 つのものにまとめる」終端操作です。
ざっくりとした使い分けはこうです。
合計・最大値・フラグの AND / OR など、
「単純な値 1 つ」に畳み込みたい → reduce
List / Set / Map に集めたい、
グルーピング・集計など「コンテナに詰めたい」 → collect
もちろん、reduce でも List を作ることはできますが、
List<T> list =
stream.reduce(
new ArrayList<T>(),
(acc, e) -> { acc.add(e); return acc; },
(left, right) -> { left.addAll(right); return left; }
);
Javaのように、かなり読みにくくなります。
「コンテナに集める」処理は、素直に collect を使った方が、
意図もパフォーマンスも良くなります。
まとめ:collect の仕組みを自分の言葉で定義する
あなたの言葉で collect をまとめるなら、こうなります。
「collect は、Collector という“レシピ”に従って、
Stream の要素を List / Set / Map などのコンテナや集計結果にまとめ上げる終端操作。
Collector の中では、空の器を作り、要素を 1 つずつ詰め、必要なら器同士をマージする手順が定義されている。」
特に意識しておきたいのは、
toList / toSet / joining / groupingBy などは、全部「Collector のレシピ」であること
Collector の本質は「バケツを作る」「詰める」「くっつける」の 3 ステップであること
「値 1 つ」なら reduce、「コンテナに集める」なら collect というざっくりした使い分け
あたりです。
