CodeGym /コース /JAVA 25 SELF /forEach と peek メソッド:副作用

forEach と peek メソッド:副作用

JAVA 25 SELF
レベル 30 , レッスン 3
使用可能

1. forEach メソッド:要素に対する最終アクション

詳細に入る前に、ストリームの終端操作と中間操作とは何かをもう一度確認しましょう。

  • 中間操作(例:filtermapdistinctpeek)— 新しいストリームを返し、通常は終端操作が呼ばれるまで実行されません。
  • 終端操作(例:forEachcollectcountanyMatch)— ストリーム要素の処理を開始し、結果を返します(forEach のように何も返さない場合もあります)。

終端操作の後、ストリームは閉じられ、それ以上の操作を適用できません。食べ終わったアイスをもう一度食べようとするようなもの—できません。ストリームはすでに「使い終わり」です。

ここでは副作用のための重要な 2 つのメソッド、forEachpeek を見ていきます。

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 の違いは?

メソッド 操作の種類 適用タイミング チェーンを続けられるか? 適した用途
forEach
終端 ストリーム処理の最終段階 不可 最終処理(出力、ログ、DB への書き込み)
peek
中間 処理チェーンの途中 デバッグ、中間ログ、計数

例:使い分けの違い

// 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 は、その後に終端操作(collectforEachcount など)がなければ実行されません。終端操作がなければ何も起きません。

ミス3: forEach の後にストリームを続けようとする。 forEach は終端操作です。これ以降にストリームのメソッドは呼べません。

ミス4: forEach の中でコレクションを変更する。 forEach による走査中に元のコレクション(要素の削除や追加)を変更するのは、ConcurrentModificationException の直接の原因になります。

ミス5: 結果を集めるのに forEach を使う。 要素を新しいコレクションに集めたいなら、collect(Collectors.toList()) を使いましょう。手作業で forEach に追加するのは宣言的でなく、特にマルチスレッドのシナリオでは誤りの元になります。

1
タスク
JAVA 25 SELF, レベル 30, レッスン 3
ロック未解除
黒板への生徒の呼び出し 👩‍🏫
黒板への生徒の呼び出し 👩‍🏫
1
タスク
JAVA 25 SELF, レベル 30, レッスン 3
ロック未解除
スマートホームのデータストリームのデバッグ 🏠
スマートホームのデータストリームのデバッグ 🏠
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION