AIによる要約
Streamのfilter、map、peekなどは中間操作であり、呼び出した時点では要素処理を開始しません。toList、collect、forEach、countなどの終端操作が実行されて初めて、必要な要素が流れます。そのため終端操作がなければpeekは動かず、findFirstやlimitでは全件分動くとも限りません。peekは主に処理途中の観察へ使い、保存・更新・通知などの重要な副作用を置かないのが安全です。


Streamは、Listの各要素をすぐ加工して結果を保存する仕組みではありません。中間操作をつなげて「何をするか」を組み立て、終端操作が呼ばれた時に必要な範囲だけ実行します。
この遅延評価を知らずにpeekへ保存処理やカウンタ更新を置くと、「動かない」「一部だけ動く」「実装変更で回数が変わる」という障害になります。
この記事のポイント
- peekは中間操作なので終端操作がなければ動かない
- Streamは必要な要素だけ遅延評価する
- findFirstやlimitではpeekが全件実行されない
- 重要な副作用はpeekではなく明示的な処理へ置く
- デバッグログも要素数と個人情報に注意する
XのJavaクイズでpeekの動きを確認する

List.of("A", "B").stream()
.peek(System.out::println);
System.out.println("done");出力されるのは`done`だけです。Streamオブジェクトは作られていますが、要素を流す終端操作がありません。

中間操作と終端操作の違い
| 種類 | 代表例 | 役割 |
|---|---|---|
| 中間操作 | filter、map、peek、sorted、limit | 処理手順を組み立て、新しいStreamを返す |
| 終端操作 | toList、collect、forEach、count、findFirst、anyMatch | 結果を作るためにStreamを実行する |
List<String> result = List.of("A", "B").stream()
.peek(System.out::println)
.map(String::toLowerCase)
.toList();`toList`が終端操作なので、AとBがpeekで表示され、結果は`["a", "b"]`になります。peek自体が結果を進めるのではなく、終端操作から必要とされて実行されます。
peekは全件実行されるとは限らない
Optional<String> result = List.of("A", "B", "C").stream()
.peek(value ->
System.out.println("peek: " + value))
.filter(value -> value.equals("B"))
.findFirst();findFirstはBが見つかった時点で終了できるため、Cまで処理する必要がありません。peekはAとBで実行されますが、Cでは実行されません。
List<String> result = values.stream()
.peek(this::recordAccess)
.limit(10)
.toList();`recordAccess`を全件記録のつもりで使っても、limitの後続が必要とする件数しか実行されません。最適化や終端操作の種類に依存する重要処理をpeekへ置くべきではありません。
peekへ保存や更新を置かない
List<Order> result = orders.stream()
.filter(Order::isPending)
.peek(orderRepository::save)
.toList();動く場合はありますが、読み手はpeekを観察用と考えるため、DB保存が隠れます。終端操作が消されたりfindFirstへ変わったりすると、保存件数が変わります。トランザクション境界や例外時の扱いも分かりにくくなります。
List<Order> pendingOrders = orders.stream()
.filter(Order::isPending)
.toList();
for (Order order : pendingOrders) {
orderRepository.save(order);
}保存という重要な副作用を明示します。複数件保存なら`saveAll`が適切な場合もあります。Streamを1本にまとめることより、どこで状態が変わるかが読めることを優先します。
値の変換はmapへ書く
List<OrderDto> dtos = orders.stream()
.map(order -> toDto(order))
.toList();入力から別の値を作る処理はmapです。peek内でオブジェクトを書き換え、同じ参照を後続へ流すと、変換なのかデバッグなのか分かりません。
// 避けたい例
orders.stream()
.peek(order -> order.setStatus("SENT"))
.toList();
// 意図を明示する例
orders.forEach(order -> order.changeStatus(SENT));Entityの状態変更が業務処理なら、Serviceメソッドやドメインメソッドとして名前を付けます。JPAのダーティチェックで更新される場合も、peekに隠すとレビューで見落とされます。
デバッグ用途でもログ量に注意する
List<OrderDto> result = orders.stream()
.peek(order ->
log.debug("変換前 orderId={}", order.getId()))
.map(this::toDto)
.peek(dto ->
log.debug("変換後 orderId={}", dto.id()))
.toList();変換前後を一時的に確認する用途ならpeekは便利です。ただし大量データで1件ずつログを出すと、ログ量と性能へ影響します。Entity全体を出せば個人情報が混ざる可能性もあります。必要なIDだけに絞り、調査後に残すべきログか見直します。
同じStreamを2回使うことはできない
Stream<String> stream = values.stream()
.peek(System.out::println);
long count = stream.count();
List<String> list = stream.toList();終端操作を実行したStreamは消費済みです。2回目の`toList`で`IllegalStateException`になります。再利用したい場合は元のCollectionからStreamを作り直すか、1回目の結果をListへ保存します。
テストでは結果と副作用を分ける
@Test
void 終端操作がなければpeekは実行されない() {
AtomicInteger calls = new AtomicInteger();
Stream<String> stream = List.of("A", "B").stream()
.peek(value -> calls.incrementAndGet());
assertEquals(0, calls.get());
}
@Test
void findFirstは必要な位置まで処理する() {
AtomicInteger calls = new AtomicInteger();
Optional<String> result = List.of("A", "B", "C").stream()
.peek(value -> calls.incrementAndGet())
.filter("B"::equals)
.findFirst();
assertEquals(Optional.of("B"), result);
assertEquals(2, calls.get());
}このテストはStreamの性質を理解する学習用です。業務テストではpeekの回数に依存する設計を避け、最終結果や明示的なService呼び出しを検証します。
現場レビューでよくある指摘
// レビューコメント例
終端操作がないため、このStreamとpeekは実行されません。
結果をどこで利用する設計か確認してください。
// レビューコメント例
peek内にRepository保存があり、副作用が見えにくくなっています。
対象抽出と保存処理を分けてください。
// レビューコメント例
findFirstで短絡するため、peekが全件実行される保証はありません。
全件処理が要件なら別の処理として明示してください。提出前のセルフチェック
- 終端操作があるか
- peekの実行回数へ業務処理が依存していないか
- findFirst・anyMatch・limitで短絡しないか
- 保存・更新・通知をpeekへ置いていないか
- 変換処理はmapで表せないか
- ログ量と機密情報を確認したか
- 消費済みStreamを再利用していないか
- for文の方が明確ではないか
Streamを実践的に学ぶ参考書
Streamは中間操作と終端操作、遅延評価を一緒に理解すると、短いコードに引っ張られず適切に使えます。
スッキリわかるJava入門 実践編 第5版
基礎文法の次に必要な、現場寄りのJava知識を補う。
コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。
- Java基礎の次に何を学ぶか迷っている
- コレクションやStreamを整理したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Stream.peekは中間操作なので、終端操作がなければ実行されません。またfindFirstやlimitなどで処理が短絡すれば、全要素に対して動くとは限りません。
peekは処理途中の観察に限定し、保存・更新・通知などの副作用を置かないようにします。変換はmap、全件処理はforEachや明示的なループへ分け、実行タイミングが読めるコードにしましょう。
