Java | Java 詳細・モダン文法:Stream API 深掘り – collect の仕組み

Java Java
スポンサーリンク

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

ここで起きていることは、

  1. 空の List を用意する
  2. Stream の要素を 1 つずつその List に add していく
  3. 最後にその 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 つ」か「コンテナ」か

reducecollect も、
「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 というざっくりした使い分け

あたりです。

タイトルとURLをコピーしました