Stream が「再利用できない」ってどういうことか
まず結論から言うと、Java の Stream は
「一度“流して”終端操作を呼んだら、そのストリームは二度と使えない(使い捨て)」
という性質を持っています。
同じストリーム変数に対して、count() を呼んだあとに forEach() を呼ぶ、みたいなことはできません。
やろうとすると、実行時に IllegalStateException: stream has already been operated upon or closed が投げられます。
ここをしっかり理解しておくと、「あれ?さっきまで動いていたのに例外になった…」という事故を避けられます。
典型例:一度使った Stream をもう一度使おうとして怒られる
実際にエラーになるコード
まずは「やってはいけないパターン」をコードで見てみます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamReuseError {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
Stream<Integer> stream = list.stream();
long count = stream.count();
System.out.println("count = " + count);
stream.forEach(System.out::println); // ここで例外
}
}
Javaこのコードを実行すると、最初の count は正常に動きますが、
そのあと forEach を呼んだところで、次のような例外が発生します。
java.lang.IllegalStateException: stream has already been operated upon or closed
メッセージの通り、「このストリームはすでに操作されたか閉じられている」ので、
もう一度使おうとすると怒られるわけです。
なぜこうなるのか(ざっくりした理由)
count() は「終端操作」です。
終端操作が呼ばれた時点で、そのストリームは「最後まで流し切られた」状態になります。
ストリーム内部のリソース(イテレータやファイルハンドルなど)は消費され、
パイプラインも「クローズ済み」として扱われます。
だから、そのストリーム変数に対して再度 forEach() などの終端操作を呼ぶことはできません。
ストリームは「一度きりの川」だとイメージしてください。
一度水を流し切った川を、そのままもう一度ゼロから流すことはできない、という感覚です。
再利用不可性の背景:ストリームは「遅延評価の一度きりパイプライン」
遅延評価と「その場で流して終わり」の関係
Stream は「遅延評価」でした。filter や map をつなげただけでは実行されず、forEach や collect などの終端操作を呼んだ瞬間に、初めて要素が流れ始めます。
実行されるときのイメージはこうです。
- ソース(List や配列、ファイルなど)から 1 要素取り出す
- その要素を
filterやmapのパイプラインに通して、終端操作まで届ける - 次の要素を取り出して同じことを繰り返す
- ソースの要素が尽きたらストリームは終わり
この「ソースから要素を取り出す」処理は、たいてい一方向に一度だけ進める仕組みになっています。
例えば、List の内部イテレータや、ファイルの読み取り位置などです。
一度最後まで読み切ったイテレータを、戻さずにもう一度最初から使うことはできませんよね。
ストリームも同じで、「一度流し切ったら終わり」です。
ソース側のリソース解放という観点
特に、ファイル・ソケット・データベースなど外部リソースに紐づいたストリームは、
終端操作が呼ばれた時点で内部的にクローズされることが多いです。
例えば Files.lines(path) で作ったストリームは、try-with-resources などで終端操作まで実行したら、自動的にクローズされます。
閉じたファイルストリームを再度読もうとしたらおかしい――
それと同じで、「クローズ済みの Stream は再利用不可」というルールになっています。
「Stream を再利用したくなる」よくあるパターンと、正しい書き方
よくある誤パターン:同じ Stream 変数を何度も使う
例えば、次のようなニーズがあります。
「この List の要素数を数えたい(count)」
「同じ対象を forEach で表示もしたい」
初心者がやりがちなのは、さきほどのように同じ Stream で両方やろうとする書き方です。
Stream<Integer> stream = list.stream();
long count = stream.count();
stream.forEach(...); // ダメ
Java正しい発想は、「Stream そのものを再利用しようとしない」ことです。
解決策1:毎回新しい Stream を生成する
List や配列など、元データが再利用できるなら、
必要なタイミングで毎回 list.stream() を呼んで新しい Stream を作ればよいです。
List<Integer> list = Arrays.asList(1, 2, 3);
// 1回目のストリーム(要素数を数える)
long count = list.stream().count();
System.out.println("count = " + count);
// 2回目のストリーム(表示)
list.stream()
.forEach(System.out::println);
Javaポイントは、「stream() を呼ぶのは安い操作」だということです。stream() は大抵、「内部のイテレータをくれるだけ」のような軽い処理です。
「Stream を変数に保存して再利用しよう」と考えるのではなく、
「元コレクションから必要なときに何度でもストリームを作る」と考えると安全です。
解決策2:一度 List に collect してから、そこから何度も Stream にする
もしソースが「一度しか読めない」タイプ(例えば Files.lines(path) やネットワークストリーム)なら、
最初に全部 List に読み込んでおくという手もあります。
List<String> lines;
try (Stream<String> lineStream = Files.lines(path)) {
lines = lineStream.toList(); // ここでファイルを全部読み込んで閉じる
}
// 以降は lines から何度でも stream() できる
long count = lines.stream().count();
lines.stream().forEach(System.out::println);
Java一度 List にしてしまえば、lines.stream() は何度呼んでもかまいません。
ストリームを無理に再利用しようとするのではなく、
「再利用したいなら、再利用できる形(List や配列)に変換しておく」という発想になります。
「再利用不可」を意識して設計するときのコツ
Stream をフィールドに持たない、保持しない
クラスのフィールドに Stream を持たせてしまうと、再利用不可性で簡単にハマります。
例えば、こんな設計は危険です。
class UserService {
private Stream<User> userStream;
UserService(List<User> users) {
this.userStream = users.stream();
}
long countActive() {
return userStream.filter(User::isActive).count();
}
void printAll() {
userStream.forEach(System.out::println); // ここで例外になる可能性大
}
}
JavacountActive で一度ストリームを流してしまったら、printAll を呼んだときには userStream はすでに「使い切り済み」です。
こういったバグは、最初は静かに潜んでいて、
特定のメソッド呼び出し順でだけ爆発するので、非常にやっかいです。
設計としては、
Stream をフィールドや長生きする変数に保持しない
必要なメソッドの中で、その都度 collection.stream() を呼ぶ
とするのが安全です。
「パイプライン全体」を一気に書いて、その場で終わらせる
よい書き方の基本は、
「stream() から終端操作までを、一つの式で完結させる」
ことです。
例えば、
long count =
users.stream()
.filter(User::isActive)
.count();
Javaあるいは、
users.stream()
.filter(User::isActive)
.forEach(System.out::println);
Javaこのように、「このストリームはここで終わり」と明確にしてあげるのがベストです。
Stream 変数を中途半端に外に出しておくと、
「このストリームはもう使っていいのか、ダメなのか」が一気に曖昧になります。
Supplier を使って「必要なときに新しい Stream を供給する」テクニック(応用)
少し応用になりますが、
「同じ“パイプライン”を複数回使いたい」というニーズもあります。
例えば、
「同じフィルタ条件で、count も取りたいし、List も作りたい」
という場合です。
ダメな例:
Stream<User> filtered =
users.stream()
.filter(User::isActive);
long count = filtered.count();
List<User> list = filtered.toList(); // ここで例外
Javaこれをきれいに書く方法の一つとして、Supplier<Stream<T>> を使うやり方があります。
import java.util.function.Supplier;
Supplier<Stream<User>> filteredStreamSupplier =
() -> users.stream().filter(User::isActive);
long count = filteredStreamSupplier.get().count();
List<User> list = filteredStreamSupplier.get().toList();
Javaget() を呼ぶたびに「新しい Stream」が生成されるので、
count 用と toList 用で、それぞれ別のストリームになります。
この方法は少し上級寄りですが、
「Stream を保持するのではなく、“Stream を作るレシピ”を保持する」
という設計のヒントとして覚えておくと役に立ちます。
まとめ:Stream の再利用不可性を自分の中に落とし込む
Stream の再利用不可性を、初心者向けにまとめるとこうです。
- Stream は「一度きりの流れ」。終端操作を呼んだら、そのストリームは二度と使えない。
- count と forEach を同じ Stream で両方やろうとすると、IllegalStateException になる。
- 複数回処理したいときは、毎回
collection.stream()から新しい Stream を作る。 - ソースが一度しか読めない場合は、まず List などに collect してから、そこから何度でも stream() する。
- Stream をフィールドなどで長期間保持せず、「メソッドの中で作って、その中で終わらせる」書き方を基本にする。
