collect で集める

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

collect で集める とは

Stream の終端操作 collect と Collectors の代表的なファクトリ (toList, toSet, toMap, joining) を学び、filter で絞り込んだ要素を joining で文字列。

collectStream を最終形に変換する

Stream を使った処理は、源流 (source) → 中間操作 (filter map など) → 終端操作 という 3 段構えで組み立てます。これまでのレッスンでは終端操作として forEachcount を扱ってきましたが、現場で圧倒的に出番が多いのは今回紹介する collect です。collect は流れてきた要素を集めて ListSetMap、文字列など「具体的な入れ物」に詰め直す万能の終端操作で、Stream の旅の終着点と言えます。

diagram (will load when visible)

図の右端を見てください。collect の引数には Collector というオブジェクトを渡しますが、自分で Collector を作る必要は (最初のうちは) ありません。java.util.stream.Collectors というユーティリティクラスが、典型的な集約処理をすぐに使える形で用意してくれているので、ほぼ全ての用途はそこから選べば事足ります。

collectStream の中で最も自由度が高い終端操作です。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> (中身が CharSequenceStream) を String 1 つに連結してくれる Collector で、String.joinStream 版と思えば 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 で囲み付き、というふうに用途で使い分けます。今回の課題は "," 区切りなので、真ん中のパターンを使います。

ここで注意したいのが、joiningStream<String> (文字列の Stream) でしか使えない ことです。IntStreamStream<Integer> をそのまま joining に渡すとコンパイルエラーになるので、mapToObj(String::valueOf)map(Integer::toString)先に文字列化してから collect(Collectors.joining(...)) を呼ぶ、という二段構えになります。今回の課題コードでも、まず IntStreammapToObjStream<String> に変えるところがポイントです。

Collectors.joining を呼ぶ前は必ず Stream<String> になっているか確認しましょう。int のままだと joining が見つからずエラー、というのは初学者がよくハマるところです。

標準 Collector のラインナップ

Collectors にはほかにもたくさんのファクトリがあります。代表的なものを表で並べておきます。

Collector何を作るか典型的な用途
toList()List<T>リストにまとめる定番
toSet()Set<T>重複削除しつつ集合化
toMap(k, v)Map<K,V>キー → 値の対応表を作る
joining(sep)StringCSV や URL クエリ文字列の組み立て
counting()Long件数集計 (グルーピングと併用)
summingInt(f)Integer合計値を計算
averagingInt(f)Double平均値を計算
groupingBy(f)Map<K,List<V>>キー別にグループ化
partitioningBy(p)Map<Boolean,List<V>>真偽で 2 つに分ける

groupingBycounting を組み合わせて「カテゴリごとに件数を数える」、groupingBysummingInt で「カテゴリごとの売上合計」など、組み合わせで強力な集計ができるのが collect の魅力です。SQL の GROUP BY を Java で書ける、と表現してもいいでしょう。

diagram (will load when visible)

自作 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) で「重複したら先勝ち」のように指定するか、そもそも groupingByList にまとめるのが定石です
  • joiningdelimiter 指定忘れCollectors.joining() を引数なしで呼ぶと区切り文字なしで全部くっつきます。CSV のつもりで joining() と書いてしまうと "246" のようになるので、joining(",") を渡すのを忘れないようにしましょう
  • toList() の戻り値を変更しようとする — Java 16 から増えた Stream.toList() (引数なし) は immutableList を返します。後で add したいなら Collectors.toList() を使うか、new ArrayList<>(immutableList) で詰め替えてください

もう 1 つ言っておくと、IntStreamStream<Integer> に変えたいときは boxed()Stream<String> に変えたいときは mapToObj(String::valueOf) を使う、というのも joining と組み合わせる際の必修テクニックです。

一度書いた Stream パイプラインは再利用できません (stream has already been operated upon or closed)。同じデータを二度使いたいなら、毎回 nums.stream() のように新しい Stream を作るのが基本です。

やってみよう

今回の課題は、配列の中から偶数だけを残して , で連結する evensJoined メソッドの実装です。

  1. Arrays.stream(arr)IntStream を作る
  2. filter(n -> n % 2 == 0) で偶数だけ残す
  3. mapToObj(String::valueOf)Stream<String> に変換する
  4. 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 行のパイプラインで書ききってみてください。書けたら次は ," / " に変えてみたり、filtern -> 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 つができることを確認しましょう。

  1. collect の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. collect とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Arrays.stream(arr)IntStream を作ること
  2. filter で偶数 (n % 2 == 0) だけ残すこと
  3. 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"

ヒント

main.java
main.java
学習モード

メモ

collect で集める

⌘S で保存