collect で集める
collect で集める とは
Stream の終端操作 collect と Collectors の代表的なファクトリ (toList, toSet, toMap, joining) を学び、filter で絞り込んだ要素を joining で文字列。
collect で Stream を最終形に変換する
Stream を使った処理は、源流 (source) → 中間操作 (filter map など) → 終端操作 という 3 段構えで組み立てます。これまでのレッスンでは終端操作として forEach や count を扱ってきましたが、現場で圧倒的に出番が多いのは今回紹介する collect です。collect は流れてきた要素を集めて List や Set、Map、文字列など「具体的な入れ物」に詰め直す万能の終端操作で、Stream の旅の終着点と言えます。
図の右端を見てください。collect の引数には Collector というオブジェクトを渡しますが、自分で Collector を作る必要は (最初のうちは) ありません。java.util.stream.Collectors というユーティリティクラスが、典型的な集約処理をすぐに使える形で用意してくれているので、ほぼ全ての用途はそこから選べば事足ります。
collectはStreamの中で最も自由度が高い終端操作です。toListのような「形を変えるだけ」の用途から、グルーピング、集計、文字列結合まで、Collectorを差し替えるだけで全部できる、と覚えておいてください。
本章ではまずこの Collectors の代表選手を一気に押さえ、最後に Stream で偶数だけ残して , で連結する、というよくあるパターンを joining で書ききるところまでやっていきます。
Collectors.toList toSet toMap
最も基本となる 3 つは、リスト・集合・マップを作る toList toSet toMap です。書き方は次のとおりです。
Java
import java.util.*;
import java.util.stream.*;
public class Demo {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 2, 1);
List<Integer> list = nums.stream()
.filter(n -> n > 1)
.collect(Collectors.toList());
Set<Integer> set = nums.stream()
.collect(Collectors.toSet());
Map<Integer, Integer> map = nums.stream()
.distinct()
.collect(Collectors.toMap(n -> n, n -> n * n));
System.out.println(list); // [2, 3, 2]
System.out.println(set); // [1, 2, 3]
System.out.println(map); // {1=1, 2=4, 3=9}
}
}toList() は List<T> を返します。Java 16 以降では Stream 自身に toList() メソッドが生えたので、.collect(Collectors.toList()) の代わりに .toList() と書く手もありますが、こちらは戻り値が immutable (変更不可) になるという違いがあります。Collectors.toList() の方は実装上 ArrayList を返すので、後から add で要素を足したいなら従来の書き方が安全です。
toSet() は Set<T> を返します。重複が自動で消えるので、List から重複削除したいときの定番です。順序は保証されません。
toMap(keyMapper, valueMapper) は Map<K, V> を作ります。各要素から「キーをどう作るか」「値をどう作るか」をラムダで渡します。後述する キーの重複 だけは気をつける必要があります。
Collectorsの各メソッドはstaticなので、import static java.util.stream.Collectors.*;と書けばtoList()toSet()のようにCollectors.を省略して書けます。コードが短くなって読みやすいので、慣れてきたらstatic importの活用もおすすめです。
Collectors.joining で文字列に連結する
今回の課題で使うのが joining です。これは Stream<String> (中身が CharSequence の Stream) を String 1 つに連結してくれる Collector で、String.join の Stream 版と思えば OK です。
Java
import java.util.*;
import java.util.stream.*;
public class Demo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Carol");
String a = names.stream().collect(Collectors.joining());
// "AliceBobCarol"
String b = names.stream().collect(Collectors.joining(","));
// "Alice,Bob,Carol"
String c = names.stream().collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Carol]"
}
}引数のパターンは 3 種類です。引数なしならただ連結、delimiter 1 つなら区切り文字付き、3 つなら delimiter + prefix + suffix で囲み付き、というふうに用途で使い分けます。今回の課題は "," 区切りなので、真ん中のパターンを使います。
ここで注意したいのが、joining は Stream<String> (文字列の Stream) でしか使えない ことです。IntStream や Stream<Integer> をそのまま joining に渡すとコンパイルエラーになるので、mapToObj(String::valueOf) や map(Integer::toString) で 先に文字列化してから collect(Collectors.joining(...)) を呼ぶ、という二段構えになります。今回の課題コードでも、まず IntStream を mapToObj で Stream<String> に変えるところがポイントです。
Collectors.joiningを呼ぶ前は必ずStream<String>になっているか確認しましょう。intのままだとjoiningが見つからずエラー、というのは初学者がよくハマるところです。
標準 Collector のラインナップ
Collectors にはほかにもたくさんのファクトリがあります。代表的なものを表で並べておきます。
Collector | 何を作るか | 典型的な用途 |
|---|---|---|
toList() | List<T> | リストにまとめる定番 |
toSet() | Set<T> | 重複削除しつつ集合化 |
toMap(k, v) | Map<K,V> | キー → 値の対応表を作る |
joining(sep) | String | CSV や URL クエリ文字列の組み立て |
counting() | Long | 件数集計 (グルーピングと併用) |
summingInt(f) | Integer | 合計値を計算 |
averagingInt(f) | Double | 平均値を計算 |
groupingBy(f) | Map<K,List<V>> | キー別にグループ化 |
partitioningBy(p) | Map<Boolean,List<V>> | 真偽で 2 つに分ける |
groupingBy と counting を組み合わせて「カテゴリごとに件数を数える」、groupingBy と summingInt で「カテゴリごとの売上合計」など、組み合わせで強力な集計ができるのが collect の魅力です。SQL の GROUP BY を Java で書ける、と表現してもいいでしょう。
自作 Collector の予告
Collectors のメソッドで足りない処理があれば、Collector.of(...) を使って自作することもできます。引数として「空の入れ物を作る supplier」「要素を入れ物に追加する accumulator」「並列処理用の結合 combiner」「最後の仕上げ finisher」を渡すと、独自の Collector ができあがります。
標準で用意されている Collector がほぼ何でもできるので、自作する場面は実務でもそう多くはありませんが、たとえば「途中経過の JSON を組み立てる Collector」のようにドメイン固有の集約を再利用したいときに便利です。今は「Collector は自作もできるんだな」程度で覚えておいて、もう少し Stream に慣れてから挑戦すれば十分です。
自作
Collectorは中級者向けトピックなので、本コースでは概念紹介だけにとどめます。実際に書きたくなったらCollector.ofのシグネチャを公式 Javadoc で確認しましょう。
よくある間違い
collect 周りでつまずきやすいポイントを 3 つ紹介しておきます。
toMapのキー重複 —toMap(keyMapper, valueMapper)でキーが重複するとIllegalStateException: Duplicate keyが飛びます。マージ方法を決めるには 3 引数版のtoMap(k, v, (a, b) -> a)で「重複したら先勝ち」のように指定するか、そもそもgroupingByでListにまとめるのが定石ですjoiningのdelimiter指定忘れ —Collectors.joining()を引数なしで呼ぶと区切り文字なしで全部くっつきます。CSV のつもりでjoining()と書いてしまうと"246"のようになるので、joining(",")を渡すのを忘れないようにしましょうtoList()の戻り値を変更しようとする — Java 16 から増えたStream.toList()(引数なし) はimmutableなListを返します。後でaddしたいならCollectors.toList()を使うか、new ArrayList<>(immutableList)で詰め替えてください
もう 1 つ言っておくと、IntStream を Stream<Integer> に変えたいときは boxed()、Stream<String> に変えたいときは mapToObj(String::valueOf) を使う、というのも joining と組み合わせる際の必修テクニックです。
一度書いた
Streamパイプラインは再利用できません (stream has already been operated upon or closed)。同じデータを二度使いたいなら、毎回nums.stream()のように新しいStreamを作るのが基本です。
やってみよう
今回の課題は、配列の中から偶数だけを残して , で連結する evensJoined メソッドの実装です。
Arrays.stream(arr)でIntStreamを作るfilter(n -> n % 2 == 0)で偶数だけ残すmapToObj(String::valueOf)でStream<String>に変換するcollect(Collectors.joining(","))で,区切りの 1 つのStringにまとめる
テストでは戻り値の String を直接比較します。evensJoined(new int[]{1,2,3,4,5,6}) なら "2,4,6"、evensJoined(new int[]{1,3,5}) なら空文字列 ""、空配列でも "" を返します。
Collectors.joining は要素が 0 件のときに自動で空文字列を返してくれるので、空配列のケースを自分で if 分岐させる必要はありません。Stream の威力を実感できるところなので、ぜひそのまま 1 行のパイプラインで書ききってみてください。書けたら次は , を " / " に変えてみたり、filter を n -> n > 3 に変えてみたりして、Stream パイプラインの組み換えやすさを体感してみましょう。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は sorted で並び替え で、sorted で並び替え を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- collect の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. collect とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Arrays.stream(arr)でIntStreamを作ることfilterで偶数 (n % 2 == 0) だけ残すことmapToObj(String::valueOf)でStream<String>に変換してからcollect(Collectors.joining(","))で連結すること
入出力例
test-cases.txt
evensJoined([1,2,3,4,5,6]) → "2,4,6"
evensJoined([1,3,5]) → ""
evensJoined([]) → ""
evensJoined([2]) → "2"
evensJoined([-4,-3,-2,-1,0]) → "-4,-2,0"