1. forEach メソッド:要素に対する最終アクション
詳細に入る前に、ストリームの終端操作と中間操作とは何かをもう一度確認しましょう。
- 中間操作(例:filter、map、distinct、peek)— 新しいストリームを返し、通常は終端操作が呼ばれるまで実行されません。
- 終端操作(例:forEach、collect、count、anyMatch)— ストリーム要素の処理を開始し、結果を返します(forEach のように何も返さない場合もあります)。
終端操作の後、ストリームは閉じられ、それ以上の操作を適用できません。食べ終わったアイスをもう一度食べようとするようなもの—できません。ストリームはすでに「使い終わり」です。
ここでは副作用のための重要な 2 つのメソッド、forEach と peek を見ていきます。
forEach は何をするのか
forEach はストリームの終端操作で、ストリーム内の各要素に対して指定の処理を実行します。画面出力、ログ記録、統計の集計など、さまざまな副作用(side effects)に使われます。
メソッドのシグネチャ:
void forEach(Consumer<? super T> action)
Consumer<T> は、引数を 1 つ受け取り値を返さない関数型インターフェースです(例:System.out::println)。
例:リストの全要素を出力する
ユーザー名のリストがあるとします。
List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");
Stream API を使えば、全ユーザーを画面に出力するのは簡単です。
users.stream().forEach(System.out::println);
結果:
Anna
Boris
Alex
Alina
Dmitry
ラムダ式を使うこともできます。
users.stream().forEach(name -> System.out.println("ユーザー: " + name));
結果:
ユーザー: Anna
ユーザー: Boris
ユーザー: Alex
ユーザー: Alina
ユーザー: Dmitry
重要: forEach はストリームを終端する
forEach を呼ぶと、ストリームは「閉じます」。チェーンを続けることはできません。
users.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println)
.map(String::toUpperCase); // エラー!ストリームはすでに終端済み。
forEach の後に何かを呼ぼうとするとコンパイルエラーになります。終端操作は void を返し、新しいストリームは返しません。
2. peek メソッド:覗き見るが、介入はしない
peek は中間操作です。処理のある段階で各要素に対して何らかの処理を実行できますが、要素自体を変更せず、ストリームも終端しません。
メソッドのシグネチャ:
Stream<T> peek(Consumer<? super T> action)
- peek は新しいストリームを返し、各要素に対して action を実行します。
- 主にデバッグ、ログ出力、ストリームの状態監視に使われます。
例:フィルタ後のログ出力
List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");
List<Integer> nameLengths = users.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("フィルタ通過: " + name))
.map(String::length)
.collect(Collectors.toList());
コンソール出力:
フィルタ通過: Anna
フィルタ通過: Alex
フィルタ通過: Alina
nameLengths の内容:
[4, 4, 5]
peek を使うと便利な場面
- 操作チェーンのデバッグ:各段階で何が起きているかを確認したいとき。
- 統計の収集(例:要素数のカウント)。
- 中間段階でのログ出力。
重要: peek はストリーム要素を変更するために使うべきではありません。変換には map を使いましょう。peek は「覗く」ためのもので、「介入」するためのものではありません。
3. forEach と peek の違いは?
| メソッド | 操作の種類 | 適用タイミング | チェーンを続けられるか? | 適した用途 |
|---|---|---|---|---|
|
終端 | ストリーム処理の最終段階 | 不可 | 最終処理(出力、ログ、DB への書き込み) |
|
中間 | 処理チェーンの途中 | 可 | デバッグ、中間ログ、計数 |
例:使い分けの違い
// forEach の例
users.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println); // ここでストリームは終端される
// peek の例
users.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("フィルタ通過: " + name))
.map(String::toUpperCase)
.collect(Collectors.toList()); // チェーンを続けられる
覚えておきたいこと
- forEach は戻れないポイントです。これ以降はストリームに何もできません。
- peek は、終端操作が呼ばれない限り実行が保証されません。中間操作だけを並べても、何も起きません。
4. 一見わかりにくい点:forEach が常に最良とは限らない!
なぜ forEach でコレクションを変更すべきではないのか?
多くの初心者は forEach でコレクションの要素やコレクション自体(要素の削除など)を変更しようとします。しかしこれは悪いプラクティスです。ストリームは元のコレクションを変更するためのものではありません。
誤った使い方の例:
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // ConcurrentModificationException を引き起こす可能性があります!
}
});
結果: 実行時エラー—ストリームの走査中にコレクションを変更してはいけません(ConcurrentModificationException)。
forEach は結局どんな時に使うべき?
- 画面出力(例:レポートの印刷)。
- ログ記録。
- 外部サービスの呼び出し(例:メール送信)。
- 統計の収集(例:カウンタの増加)。
5. peek についてもう一度:基本はデバッグ用!
peek を使って要素を変更したくなることがあります。例えばユーザーの年齢を 1 増やすなど。
users.stream()
.peek(user -> user.setAge(user.getAge() + 1)) // よくない!
.collect(Collectors.toList());
なぜ悪いのか?
- Stream API の宣言的スタイルや純度を損なうため。
- 保守やテストが難しくなるため。
- 中間操作での副作用は、思わぬバグにつながる可能性があるため。
データの変換には map を使うほうが良いです。
List<User> olderUsers = users.stream()
.map(user -> new User(user.getName(), user.getAge() + 1))
.collect(Collectors.toList());
図:forEach と peek の違い
users.stream()
.filter(...) // 中間操作
.peek(...) // 中間操作、"覗き見"
.map(...) // 中間操作
.forEach(...) // 終端操作、"アクションを実行"
補足:
— forEach までの処理は、組み合わせたり、順序を変えたり、追加したりできます。
— forEach の後はストリームは閉じています。
6. forEach と peek でよくあるミス
ミス1: peek をデータ変更に使う。 peek は観察用であり、要素を変更するものではありません。変換には map を使いましょう。
ミス2: peek が必ず実行されると期待する。 peek は、その後に終端操作(collect、forEach、count など)がなければ実行されません。終端操作がなければ何も起きません。
ミス3: forEach の後にストリームを続けようとする。 forEach は終端操作です。これ以降にストリームのメソッドは呼べません。
ミス4: forEach の中でコレクションを変更する。 forEach による走査中に元のコレクション(要素の削除や追加)を変更するのは、ConcurrentModificationException の直接の原因になります。
ミス5: 結果を集めるのに forEach を使う。 要素を新しいコレクションに集めたいなら、collect(Collectors.toList()) を使いましょう。手作業で forEach に追加するのは宣言的でなく、特にマルチスレッドのシナリオでは誤りの元になります。
GO TO FULL VERSION