Stream を作る
このレッスンで分かること
Streamは「データの流れ」を表すパイプ。コレクションをstream()で流し始めるStream.of(1, 2, 3)Arrays.stream(arr)Stream.iterateStream.generateで生成- Stream は使い捨て (
reuse 禁止)。一度終端操作したら再利用できない- 中間操作 (
mapfilter) は遅延評価、終端操作 (collectcount等) で初めて流れる
Stream を作る とは
Java の Stream API を学ぶ最初のステップとして、配列やコレクションから Stream を生成し、count などのターミナル操作で要素数を取得するコードを書いてみよう。
Stream API ってなに?
Java 8 で導入された Stream API は、配列やコレクションを「宣言的に」処理するための仕組みです。これまで for ループや for-each で 1 件ずつ要素を見ていた処理を、流れ (stream) として捉え直して、「ふるいにかける」「形を変える」「数える」のような操作を メソッドチェーン でつなげて書けるようにします。
Streamは SQL のクエリや関数型言語のmapfilterreduceに近い発想です。「どう処理するか」ではなく「何をしたいか」を書く、いわゆる宣言的プログラミングのスタイルです。これまで命令的に書いてきた人ほど、最初は違和感があるかもしれませんが、慣れると圧倒的に読みやすくなります。
具体的な例で比べてみましょう。たとえば「配列 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()—ListSetなどコレクションから作る。最も一般的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 になります。IntStream は sum() average() max() などプリミティブ向けの便利メソッドを持っているので、int[] を扱うときは Arrays.stream 経由で IntStream を作るのが定番です。
double[]にはDoubleStream、long[]にはLongStreamという対応する Stream があります。プリミティブ型ごとに専用 Stream が用意されているのは、Stream<Integer>のように毎回オートボクシングするとパフォーマンスが落ちるからです。基本は元のデータの型に合わせた Stream を選びます。
ターミナル操作 — Stream を「使い切る」
Stream を作っただけでは何も起きません。filter や map などの 中間操作 をつないだあとに、最後に ターミナル操作 を呼んで初めて処理が走ります。これを 遅延評価 (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 を返します。今回の課題でも long を int に渡す場面が出てきます。
図のように、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 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です CollectionとStreamを混同する —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 倍速い- 遅延評価で短絡 ─
findFirstanyMatchは条件を満たした時点で止まるので、filter後にすぐ呼ぶと無駄が少ない
ここまでの要点 Stream は「データの流れ」、生成→中間操作→終端操作で 1 回限りの処理。並列化やプリミティブ stream を場面で使い分ける。
やってみよう
それでは今回の課題に挑戦しましょう。やることは次のとおりです。
- ファイル先頭で
import java.util.Arrays;を書く Solution.streamCount(int[] arr)の中でArrays.stream(arr)を呼んでIntStreamを作るcount()を呼んで要素数を取得する (戻り値はlong)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 つができることを確認しましょう。
- Stream 生成 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Stream 生成 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.util.Arrays;を書くこと Arrays.stream(arr)でIntStreamを生成し、count()で要素数を取得すること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