Stream を作る

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

このレッスンで分かること

  • Stream は「データの流れ」を表すパイプ。コレクションを stream() で流し始める
  • Stream.of(1, 2, 3) Arrays.stream(arr) Stream.iterate Stream.generate で生成
  • Stream は使い捨て (reuse 禁止)。一度終端操作したら再利用できない
  • 中間操作 (map filter) は遅延評価、終端操作 (collect count 等) で初めて流れる

Stream を作る とは

Java の Stream API を学ぶ最初のステップとして、配列やコレクションから Stream を生成し、count などのターミナル操作で要素数を取得するコードを書いてみよう。

Stream API ってなに?

Java 8 で導入された Stream API は、配列やコレクションを「宣言的に」処理するための仕組みです。これまで for ループや for-each で 1 件ずつ要素を見ていた処理を、流れ (stream) として捉え直して、「ふるいにかける」「形を変える」「数える」のような操作を メソッドチェーン でつなげて書けるようにします。

Stream は SQL のクエリや関数型言語の map filter reduce に近い発想です。「どう処理するか」ではなく「何をしたいか」を書く、いわゆる宣言的プログラミングのスタイルです。これまで命令的に書いてきた人ほど、最初は違和感があるかもしれませんが、慣れると圧倒的に読みやすくなります。

具体的な例で比べてみましょう。たとえば「配列 arr の要素のうち偶数だけ抜き出して合計する」処理を、これまでの命令的なやり方と Stream の宣言的なやり方で書くと次のような違いになります。

Java

// 命令的な書き方 (これまでのスタイル) int sum = 0; for (int n : arr) { if (n % 2 == 0) { sum += n; } } // 宣言的な書き方 (Stream) int sum = Arrays.stream(arr) .filter(n -> n % 2 == 0) .sum();

同じ結果を返しますが、Stream 版は「arr から stream を作って、偶数だけ filter して、sum する」と、英語の文章のように読めます。コード量も短く、sum 用の変数を別途用意する必要もありません。

本章ではまず、この Streamどうやって作るか に焦点を絞って学びます。Stream そのものは「データの流れ」を表す抽象的な存在で、元データ (配列、List、固定値) から 生成 して初めて使えるようになります。

Stream の生成方法は 4 種類覚えよう

よく使う Stream の作り方は、大きく分けて次の 4 つです。それぞれ用途と書き方が違うので、まず一覧で覚えておきましょう。

  • Collection.stream()List Set などコレクションから作る。最も一般的
  • Arrays.stream(arr) — 配列から作る。int[] String[] などに使う
  • Stream.of(a, b, c) — 個別の値を直接並べて作る。テストやサンプルで便利
  • IntStream.range(start, end) — 連番の整数 Stream を作る。for (int i=0; i<n; i++) の代わり

コードで書くとこのようになります。

Java

import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; public class StreamCreateDemo { public static void main(String[] args) { // 1. List から List<String> names = List.of("Alice", "Bob", "Carol"); Stream<String> s1 = names.stream(); // 2. 配列から int[] arr = {1, 2, 3, 4, 5}; IntStream s2 = Arrays.stream(arr); // 3. 直接値から Stream<Integer> s3 = Stream.of(10, 20, 30); // 4. 連番から IntStream s4 = IntStream.range(0, 5); // 0,1,2,3,4 } }

注目してほしいのは戻り値の型です。要素が Object (String Integer など) なら Stream<T>int を扱うときは IntStream という専用の Stream になります。IntStreamsum() average() max() などプリミティブ向けの便利メソッドを持っているので、int[] を扱うときは Arrays.stream 経由で IntStream を作るのが定番です。

double[] には DoubleStreamlong[] には LongStream という対応する Stream があります。プリミティブ型ごとに専用 Stream が用意されているのは、Stream<Integer> のように毎回 オートボクシング するとパフォーマンスが落ちるからです。基本は元のデータの型に合わせた Stream を選びます。

ターミナル操作 — Stream を「使い切る」

Stream を作っただけでは何も起きません。filtermap などの 中間操作 をつないだあとに、最後に ターミナル操作 を呼んで初めて処理が走ります。これを 遅延評価 (lazy evaluation) と呼びます。

代表的なターミナル操作は次の通りです。

  • count() — 要素数を long で返す。今回の課題で使う
  • forEach(action) — 1 件ずつ処理する。戻り値なし
  • collect(Collectors.toList())List に変換する
  • sum() average() max() min()IntStream などのプリミティブ Stream 専用
  • toArray() — 配列に変換する

Java

import java.util.Arrays; public class Demo { public static void main(String[] args) { int[] arr = {3, 1, 4, 1, 5, 9, 2, 6}; long c = Arrays.stream(arr).count(); // 8 int sum = Arrays.stream(arr).sum(); // 31 Arrays.stream(arr).forEach(System.out::println); } }

注目したいのは count() の戻り値が long であることです。Stream は数十億件規模のデータを想定しているので、int (約 21 億まで) では足りなくなる可能性を考えて long を返します。今回の課題でも longint に渡す場面が出てきます。

diagram (will load when visible)

図のように、Stream 処理は 元データ → 生成 → 中間操作 (0 個以上) → ターミナル操作 (必ず 1 個) という一本の パイプライン を組み立てるイメージです。今回はまず一番シンプルな「生成 → count」だけの最短パイプラインを書きます。

Stream は一度しか使えない

ここで Stream の重要な性質をひとつ押さえておきます。それは 一度ターミナル操作を呼んだ Stream は二度と使えない ということです。再利用しようとすると IllegalStateException: stream has already been operated upon or closed という実行時エラーになります。

Java

Stream<Integer> s = Stream.of(1, 2, 3); long c1 = s.count(); // OK: 3 long c2 = s.count(); // 実行時エラー!

配列や List のように手元に残るデータではなく、Stream は「処理の流れ」そのものなので、流れ終わった水を再利用できないのと似ています。もう一度使いたいときは、元データから stream() し直す必要があります。

Java

List<Integer> list = List.of(1, 2, 3); long c1 = list.stream().count(); // OK long c2 = list.stream().count(); // OK (毎回新しい Stream を作る)

この性質があるからこそ、Stream は メモリ効率良く 動きます。中間操作の結果を全部保持しないので、巨大なデータでも軽快に処理できるのです。「使い捨て」だと割り切るのが正しい付き合い方です。

よくある間違い

Stream を使い始めた人がほぼ全員ハマる落とし穴を 3 つ紹介します。

  • Stream を使い回そうとする — 前述の通り、ターミナル操作を呼んだ Stream は閉じます。Stream<Integer> s = ...; s.count(); s.forEach(...); のように 2 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です
  • CollectionStream を混同するlist.stream() は新しい Stream を返すだけで、元の list は変わりません。list.stream().filter(...) の結果をどこにも代入せずに、「list から偶数だけ消えたはず」と思い込むのはよくあるミスです。Stream は元データを変更しません
  • ターミナル操作を忘れるArrays.stream(arr).filter(n -> n > 0); と書いて「実行された」と思い込んでも、ターミナル操作がないので 何も起きません。これは 遅延評価 のせいで、filter を呼んだだけでは実際の処理が走らないからです。count() collect() forEach() などを最後に必ず付けましょう

もう 1 つよくあるのが、int[] に対して Stream.of(arr) と書いてしまうパターンです。Stream.of(int[])1 要素の Stream<int[]> になってしまい、欲しかった IntStream にはなりません。int[] には必ず Arrays.stream(arr) を使ってください。

Stream.of は値を並べる用、Arrays.stream は配列を流す用」と用途で覚えると間違えにくいです。Stream.of(1, 2, 3) は OK、Arrays.stream(new int[]{1,2,3}) も OK、Stream.of(new int[]{1,2,3}) は NG (型が違う) です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • コレクションのバッチ変換 ─ DB 行を DTO に変換するときの定番。list.stream().map(this::toDto).toList()
  • ファイル読み込みFiles.lines(path) で巨大ファイルを 1 行ずつ Stream として読む (メモリ効率がいい)
  • 設定の重ね合わせ ─ デフォルト設定 + 環境変数 + CLI 引数を Stream.concat で順に重ねて最終設定を作る
  • 統計集計summaryStatistics()IntStream の合計 / 平均 / 最大 / 最小 を一発で取る

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • Stream を変数に保持して 2 回使う ─ 2 回目はエラー。終端操作 1 回で使い切るのがルール
  • Stream を必須でない場面で使っている ─ シンプルな for ループで済むのに無理に stream にしてかえって読みづらい
  • forEach で副作用を起こしている ─ stream 内の forEach は外部状態を変更しない設計が原則
  • 例外を中で握りつぶしているmap(x -> { try { ... } catch (...) {} }) は最悪。専用のラッパーを作る

パフォーマンス考慮事項

  • 短いコレクション (< 1000) では for ループのほうが速い ─ Stream のセットアップコストがある
  • parallelStream は要素 1 万以上で初めて意味がある ─ 小さいデータでは並列化のオーバーヘッドが上回る
  • IntStream / LongStream でボクシングを避けるStream<Integer> よりプリミティブ stream のほうが 3〜10 倍速い
  • 遅延評価で短絡findFirst anyMatch は条件を満たした時点で止まるので、filter 後にすぐ呼ぶと無駄が少ない
この章のポイント

ここまでの要点 Stream は「データの流れ」、生成→中間操作→終端操作で 1 回限りの処理。並列化やプリミティブ stream を場面で使い分ける。

やってみよう

それでは今回の課題に挑戦しましょう。やることは次のとおりです。

  1. ファイル先頭で import java.util.Arrays; を書く
  2. Solution.streamCount(int[] arr) の中で Arrays.stream(arr) を呼んで IntStream を作る
  3. count() を呼んで要素数を取得する (戻り値は long)
  4. int 型にキャストして return する (return (int) Arrays.stream(arr).count();)

テストでは streamCount(new int[]{1,2,3})3、空配列 streamCount(new int[]{})0 を返すかなどを比較します。空配列でも Arrays.stream は正常に動き、count()0 を返すので特別扱いは不要です。

慣れてきたら、count() の前に filter(n -> n > 0) を挟んで「正の数だけ数える」処理にしてみたり、sum() に変えて「合計」を取ってみたりと、いろいろなターミナル操作を試してみてください。Stream の真価は中間操作と組み合わせたときに発揮されるので、次のレッスン以降で filter map といった操作を学んでいきます。まずはここで「Stream を作って count する」という一番シンプルな流れを身につけて、Stream API の世界への第一歩を踏み出しましょう。

よくある質問

Q. Stream は for ループより遅いですか?

A. 通常規模ではほぼ同等で、可読性のメリットが上回ります。並列ストリーム(parallelStream)にすると CPU コアを活用できますが、データ量が多い純粋な計算でないとオーバーヘッドの方が大きくなるので注意してください。

Q. 中間操作と終端操作の違いは?

A. filter/map など中間操作は遅延評価で Stream を返すだけで、終端操作(collect、forEach、count 等)が呼ばれて初めて実行されます。終端操作なしの Stream は何もしないため、リファクタ時に「結果が返らない」バグを起こさないよう注意しましょう。

Q. Stream を 2 回使えますか?

A. 使えません。Stream は 1 度しか走査できないため、再利用したい場合は List に collect しておくか、Supplier<Stream<...>> として何度も生成する関数を持っておきます。「IllegalStateException: stream has already been operated upon」が典型エラーです。

次のレッスン

次は filter で絞り込む で、filter で絞り込む を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. ファイル先頭で import java.util.Arrays; を書くこと
  2. Arrays.stream(arr)IntStream を生成し、count() で要素数を取得すること
  3. count() の戻り値は long なので、(int) でキャストして int として return すること

入出力例

test-cases.txt

streamCount([1,2,3])3 streamCount([])0 streamCount([42])1 streamCount([10,20,30,40,50])5

ヒント

main.java
main.java
学習モード

メモ

Stream を作る

⌘S で保存