読み込み中...

Javaで劇的にコーディングスピードを上げる9つのforEachメソッドとループの使い方

Java forEachメソッド詳細解説 Java
この記事は約36分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

JavaのforEachは、ListMapの要素に同じ処理を適用したい場面で、通常のfor文より短く書ける構文です。そのため、出力、変換、集計、条件分岐を小さな処理としてまとめやすくなり、コーディングスピードを上げたい学習者にも扱いやすい選択肢になります。

ただし、すべてのループをforEachへ置き換える必要はありません。breakcontinueで途中制御したい処理、インデックスを強く意識する処理、例外処理が複雑な処理では、通常のfor文やstream()との使い分けが読みやすさを左右します。

このとき基準になるのは、処理の目的が副作用なのか、値の変換なのかという点です。出力や通知のような副作用ならforEach、新しいコレクションを作るならmapfilterを組み合わせる、と整理すると使い方を誤りにくくなるのが基本です。

公式ドキュメントによれば、Iterable.forEachは各要素に渡されたConsumerを適用します。詳しい仕様はOracle Java SE 17 IterableOracle Java SE 17 Streamで確認できます。

動作確認環境
  • Java 17 LTS / OpenJDK 17
  • 標準ライブラリ: java.util / java.util.stream / java.util.concurrent
  • IDEやオンライン実行環境では、クラス名をMainに合わせる構成を想定
📖 この記事で学べること
  • JavaのforEachと通常のループの使い分け
  • ListMapStreamでの基本的な使い方
  • 集計、変換、非同期処理などの応用例
  • null、例外、並列処理に関する注意点
  • 独自メソッドやフレームワーク連携によるカスタマイズ

Javaとは

Javaは、クラスを中心にプログラムを組み立てるオブジェクト指向のプログラミング言語です。その実行形式はjavac.classへコンパイルされ、JVM上で動くため、環境差を吸収しやすい構成になっています。

一般に、Javaのコードはclassmethodfieldinterfaceを組み合わせて表現します。そのため、処理を再利用しやすく、業務アプリケーション、Android関連、サーバーサイド開発などで幅広く使われますし、ここがポイントです。

これらの基礎に加えて、Java 8からはラムダ式、Stream APIdefault methodが広く使われるようになりました。forEachはその流れの中で利用頻度が高まったメソッドであり、短いループ処理を読みやすく書く入口になります。

初心者がつまずきやすいのは、拡張for文Iterable.forEachStream.forEachを同じものとして扱ってしまう点です。見た目は似ていますが、戻り値、順序、例外、並列実行時の扱いに違いがあります。

Javaの基本概念

Javaのクラスはオブジェクトの設計図にあたり、newで作られたインスタンスが実際の処理を持ちますが、これは押さえたい点です。この構造により、データと処理を近い場所へ置き、変更範囲を読み取りやすくできます。

一方、コレクションを扱う処理では、ArrayListHashMapSetなどの標準APIが頻繁に登場します。その要素を順に処理するときにforEachを選ぶと、意図を短く表しやすくなるのが目安です。

ただし、Javaは型を明示する言語なので、ラムダ式の中で受け取る値の型もコレクションの宣言に依存します。List<String>なら要素はStringとして扱われ、Map<String,Integer>ならキーと値の型が処理の前提になります。

forEachメソッドとは

forEachメソッドは、コレクション内の要素に対して同じ処理を順に適用するためのメソッドです。JavaのIterableに用意されたdefault void forEach(Consumer<? super T> action)は、要素を取り出しながらConsumer.acceptを呼び出す形で動きますし、これが一つの目安です。

具体的には、画面出力、ログ出力、既存オブジェクトのメソッド呼び出しのように、戻り値を作らず処理を進めたい場面で相性が良くなります。コーディングスピードを優先したい短い処理では、波かっこを省いたラムダ式も使えます。

💡 Tips: forEachは値を返さない終端処理として使うのが自然です。新しいリストを作る処理では、stream()filter()map()collect()を組み合わせるほうが意図を読み取りやすくなるのがポイントです。

基本の書き方

基本形は、対象のコレクションに対してforEachを呼び、引数へラムダ式を渡します。そのラムダ式の左側に要素名、右側に処理を書くため、通常のループより記述量を抑えられます。

List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(item -> {
    System.out.println(item); // 各要素を出力
});

結果: 期待される出力は、applebananacherryが順に表示される内容です。

このサンプルコードでは、Arrays.asListで作ったリストに対してforEachを呼びます。そのため、要素を取り出すためのint iget(i)を書かずに済み、短い処理ほどコーディングスピードの差が出やすくなります。

動作の概要

その動作は、拡張for文に近い形で理解できるのが一般的です。各要素がラムダ式の引数へ入り、波かっこの中に書いた処理が要素数だけ呼び出されます。

ただし、Stream.forEachIterable.forEachでは性質が異なります。parallel()を含むStreamでは処理順が保証されない場合があるため、順序が必要な処理ではforEachOrderedも候補になるのが現実的です。

apple
banana
cherry

結果: これは期待される出力例です。実際のJavaコードとして実行する部分ではなく、直前のサンプルコードで想定されるコンソール表示を表しています。

このような小さな出力処理なら、forEachの使い方は直感的です。一方、処理途中で停止する必要がある場合は、returnがラムダ式から抜けるだけで外側のメソッド全体を終了しない点に注意します。

用途主なAPI向いている処理注意点関連
単純出力forEach各要素の表示副作用中心になるList型
キーと値Map.forEachペア処理HashMapは順序保証なしエスケープ処理
条件分岐if偶数だけ表示複雑なら分離条件判定
変換map大文字化forEachだけで完結させないアノテーション
絞り込みfilter条件抽出副作用を減らすオーバーライド
集計sum合計計算AtomicIntegerの意味を理解応用例
文字列連結StringBuilder表示文作成末尾記号の処理サンプルコード
例外処理try変換失敗の吸収チェック例外は工夫が必要注意点
null対策Objects事前判定NullPointerException回避注意点
並列処理parallel独立処理順序と共有状態カスタマイズ
順序維持forEachOrdered並列時の順序速度より順序ループ
インデックスIntStream.range複数リスト対応サイズ差に注意応用例
メソッド参照System.out::println短い出力処理が単純な場合に限定使い方
外部変数final参照のみ再代入できない注意点
可変保持AtomicIntegerカウンタ通常処理では過剰な場合あり応用例
リスト更新set要素差し替え重複値に注意サンプルコード
削除処理removeIf条件削除forEach中の削除は避ける注意点
非同期CompletableFuture独立タスク完了待ちが必要応用例
DB処理JdbcTemplate行ごとの処理戻り値設計カスタマイズ
独自処理Consumer共通前処理過度な抽象化を避けるカスタマイズ
配列Arrays.stream配列の走査プリミティブ型に注意使い方
ログlogger記録処理大量出力に注意注意点
検証Predicate条件関数命名で意図を明確化応用例
変換結果collect新リスト作成終端操作の選択使い方
重複除外distinct一意化順序の前提を確認応用例
ソート後処理sorted順序付け比較関数の確認ループ
範囲生成rangeClosed連番処理終端値を確認サンプルコード
型変換parseInt文字列数値化例外処理注意点
数学処理Math.sqrt平方根NaNの扱い応用例
設計判断for途中終了読みやすさ優先使い方

forEachメソッドの使い方

JavaでforEachの使い方を覚えるときは、単純な出力から始め、次に条件分岐、外部変数、MapStreamへ広げる流れが理解しやすくなります。その順序で学ぶと、ループ処理の置き換えどころと置き換えないほうがよい箇所も見えます。

具体的には、下のサンプルコード群で、短い処理、状態を持つ処理、複数操作を含む処理を比べますが、覚えておくと役立つでしょう。どの例でも、コードブロック直後に期待される出力を示します。

サンプルコード1:基本的な使い方

基本のループ処理では、各要素を1回ずつ取り出して表示します。number -> System.out.println(number)の形なら、ラムダ式の本体が1文だけなので波かっこを省けますし、ここを基本と考えるとよいでしょう。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        numbers.forEach(number -> System.out.println(number));
    }
}

結果: 期待される出力は、1から5までの数字が順番に表示される内容です。

この形は、JavaのListに入った値を読むだけの処理に向いています。出力、ログ、単純なメソッド呼び出しのように副作用が目的なら、コーディングスピードを落とさず読みやすく書けます。

サンプルコード2:リスト内の要素を変更する

リストの要素を書き換える場合は、読み取りだけの処理より注意点が増えますし、ここがポイントです。特にindexOfは同じ値が複数あると最初の位置を返すため、重複値を含むリストでは意図しない更新になる可能性があります。

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);

        // リスト内の各要素に10を加える
        numbers.forEach(number -> numbers.set(numbers.indexOf(number), number + 10));

        System.out.println(numbers);
    }
}

結果: 期待される出力は[11, 12, 13]です。重複値がない前提では、各要素に10が加算されたリストになります。

そのため、要素更新を本格的に扱うなら、IntStream.rangeでインデックスを明示する方法や、新しいリストへ変換結果を詰める方法も候補になります。forEachだけで無理に変更処理を完結させない判断が、読みやすさを保ちますが、これは押さえたい点です。

サンプルコード3:Mapに対するforEach

Map.forEachでは、ラムダ式の引数がキーと値の2つになります。HashMapは出力順を保証しないため、順序が必要ならLinkedHashMapTreeMapを検討します。

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> students = new HashMap<>();
        students.put("太郎", 90);
        students.put("花子", 85);
        students.put("次郎", 78);

        // マップの各要素をコンソールに出力する
        students.forEach((name, score) -> {
            System.out.println(name + "の得点:" + score);
        });
    }
}

結果: 期待される出力は、各学生名と得点の組み合わせです。HashMapでは表示順がコードに書いた追加順と一致するとは限りません。

この例では、キーをname、値をscoreとして受け取ります。名前と点数のようにペアで意味を持つデータは、通常のループよりMap.forEachのほうが処理対象を読み取りやすい場合があります。

サンプルコード4:Streamと組み合わせる

Streamを使うと、処理をfiltermapforEachのように段階へ分けられますし、これが一つの目安です。この構成では、絞り込みはfilter、最後の表示はforEachが担当します。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("りんご", "みかん", "ぶどう", "さくらんぼ", "メロン");

        // ストリームを使用して、5文字以上の果物のみをコンソールに出力する
        fruits.stream()
            .filter(fruit -> fruit.length() >= 5)
            .forEach(fruit -> System.out.println(fruit));
    }
}

結果: 期待される出力は、条件に合う果物名です。文字数の数え方はJavaのString.length()が返すchar数に基づきます。

このサンプルコードのように、条件で絞ってから表示する処理は、通常のループより宣言的に読めます。一方、条件分岐が複雑になりすぎるなら、判定を別メソッドに分けると見通しが良くなると整理できます。

サンプルコード5:条件分岐を組み込む

forEachの内部にもif文を書けます。偶数だけ出力するような短い条件なら、ループ全体の意図を損なわずに表現できます。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 偶数だけを出力する
        numbers.forEach(number -> {
            if (number % 2 == 0) {
                System.out.println(number);
            }
        });
    }
}

結果: 期待される出力は246810です。

ただし、条件が増えるほどラムダ式の中が読みにくくなります。その場合は、filter(number -> number % 2 == 0)で絞り込んでからforEachで表示する書き方も検討できます。

サンプルコード6:外部変数との連携

ラムダ式から外側のローカル変数を参照する場合、その変数は実質的にfinalである必要があると理解できます。再代入はできませんが、StringBuilderのようなオブジェクトの中身を変更することはできます。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("田中", "鈴木", "佐藤");
        final StringBuilder builder = new StringBuilder();

        // StringBuilderを用いて、名前を連結する
        names.forEach(name -> {
            builder.append(name).append("さん、");
        });

        // 最後の「、」を取り除く
        builder.setLength(builder.length() - 1);

        // 結果を出力
        System.out.println(builder.toString());
    }
}

結果: 期待される出力は田中さん、鈴木さん、佐藤さんです。

この使い方では、builderという参照自体は差し替えず、内部の文字列だけを更新します。複数スレッドで共有する処理では別の考慮が必要ですが、単一スレッドの文字列組み立てなら理解しやすい例になります。

サンプルコード7:複数の操作を組み合わせる

一度のforEachで出力と集計を同時に行うこともできると覚えるとよいでしょう。このとき、外部の数値を直接再代入できないため、配列やAtomicIntegerのような保持先が使われる場合があります。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        int[] sum = {0};  // 配列を使用して、finalな変数を模倣する

        // 偶数を探し、合計を計算する
        numbers.forEach(number -> {
            if (number % 2 == 0) {
                System.out.println("偶数: " + number);  // 偶数を出力
                sum[0] += number;  // 合計に加算
            }
        });

        // 合計を出力
        System.out.println("偶数の合計: " + sum[0]);
    }
}

結果: 期待される出力は偶数: 2偶数: 4偶数の合計: 6です。

このパターンは学習用として分かりやすい一方、集計だけならstream()filter()mapToInt()sum()のほうが目的を明確に表せます。コーディングスピードだけでなく、後から読む人の理解速度も判断材料になります。

サンプルコード8:独自の処理を追加する

各要素から文字列を作り、別のリストへ入れる処理も書けると考えられます。ただし、新しいリストを作る処理はmapcollectでも表現できるため、どちらが自然かを比べるとよいでしょう。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        List<String> greetingList = new ArrayList<>();

        // 各名前に対して、挨拶文を生成し、リストに追加
        names.forEach(name -> {
            String greeting = "Hello, " + name + "!";
            System.out.println(greeting);  // 挨拶文を出力
            greetingList.add(greeting);  // 挨拶文をリストに追加
        });

        // 結果のリストを出力
        System.out.println("生成された挨拶リスト: " + greetingList);
    }
}

結果: 期待される出力は、各名前への挨拶文と、生成された挨拶リスト: [Hello, Alice!, Hello, Bob!, Hello, Charlie!]です。

このサンプルコードは、表示とリスト追加という複数の副作用を含みます。処理が増えすぎる場合は、挨拶文を作る部分をcreateGreetingのようなメソッドへ分けると、カスタマイズしやすくなります。

サンプルコード9:エラーハンドリングを含める

forEachの中でもtry-catchは書けると言えるでしょう。もっとも、下の例で使うMath.sqrtは負の数に対して例外ではなくNaNを返すため、例外処理だけでは負値の検出にならない点を理解しておく必要があります。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Double> numbers = Arrays.asList(4.0, -1.0, 9.0);

        // 数値の平方根を求める
        numbers.forEach(number -> {
            try {
                double result = Math.sqrt(number);
                System.out.println("平方根: " + result);
            } catch (Exception e) {
                System.out.println("エラー: 負の数 " + number);
            }
        });
    }
}

結果: 期待される出力は平方根: 2.0平方根: NaN平方根: 3.0です。負の数をエラー表示にしたい場合は、if (number < 0)で事前判定します。

この注意点は、JavaのAPI仕様を前提にした読み替えが必要な例です。例外が起きると決めつけず、対象メソッドが返す値と例外条件を公式ドキュメントで確認すると、誤ったサンプルコードを避けられます。

forEachメソッドの応用例

forEachの応用例では、単純なループ処理から一歩進み、集計、変換、複数リストの同期、非同期処理、オブジェクトのメソッド呼び出しを扱いるのが基本です。Javaの実務的なコードでは、これらを組み合わせる場面が多くなります。

サンプルコード1:集計処理を扱う

数値を足し込む処理では、AtomicIntegerを使って外部状態を更新できます。ただし、単純な合計ならmapToIntsumも自然な選択肢です。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        AtomicInteger sum = new AtomicInteger();

        numbers.forEach(num -> sum.addAndGet(num));

        System.out.println("合計値: " + sum.get());
    }
}

結果: 期待される出力は合計値: 15です。

この例のAtomicIntegerは、値を保持して加算する役割を持ちます。並列処理を含まない単純な集計では過剰になる場合もあるため、コードの意図に合わせて選ぶことが大切です。

サンプルコード2:フィルタリングとマッピング

文字列を条件で除外し、残った値を大文字へ変換する応用例です。forEach内でstartsWithtoUpperCaseを使い、新しいリストへ結果を追加します。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("apple", "banana", "cherry");
        List<String> upperWords = new ArrayList<>();

        words.forEach(word -> {
            if (!word.startsWith("b")) {
                upperWords.add(word.toUpperCase());
            }
        });

        System.out.println("大文字化した結果:" + upperWords);
    }
}

結果: 期待される出力は大文字化した結果:[APPLE, CHERRY]です。

一方、この処理はstream().filter(...).map(...).collect(...)でも表現できます。新しいデータを作る目的が中心なら、変換用APIを使ったほうが副作用を減らせます。

サンプルコード3:複数リストの同時処理

複数リストを同時に扱う場合、forEach単体ではインデックスを直接受け取れません。そのため、IntStream.rangeで番号を作り、その番号から各リストの要素を取得するのが目安です。

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        List<Integer> ages = Arrays.asList(25, 30, 35);

        // インデックスを用いた同時処理
        IntStream.range(0, names.size()).forEach(i -> {
            String name = names.get(i);
            int age = ages.get(i);
            System.out.println("名前: " + name + ", 年齢: " + age);
        });
    }
}

結果: 期待される出力は、同じ位置にある名前と年齢を組み合わせた行です。2つのリストの長さが異なる場合は、事前にサイズを確認します。

名前: Alice, 年齢: 25
名前: Bob, 年齢: 30
名前: Charlie, 年齢: 35

結果: これは期待される出力例です。IntStream.rangeで作ったiを使い、同じインデックスの要素を取得しています。

この応用例は、CSVの列や画面表示用データを組み合わせる場面に近い考え方です。ただし、関連する値は専用クラスへまとめたほうが、後のカスタマイズや保守では扱いやすくなるのがポイントです。

サンプルコード4:非同期処理での使い方

CompletableFuture.runAsyncを使うと、各要素に対する処理を非同期タスクとして起動できます。ただし、下のコードは完了待ちを入れていないため、実行環境によっては出力前にプログラムが終了する可能性があります。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        List<String> messages = Arrays.asList("Hello", "World", "Java");

        // 非同期でメッセージを出力
        messages.forEach(message -> CompletableFuture.runAsync(() -> {
            System.out.println("非同期メッセージ: " + message);
        }));
    }
}

結果: 期待される出力は非同期メッセージ: Hello非同期メッセージ: World非同期メッセージ: Javaです。順序は保証されない場合があります。

⚠️ 注意: 非同期処理では、タスクを起動しただけでは完了を待ちません。実用コードではCompletableFuture.allOf(...).join()のような完了待ちを設計に含めます。

サンプルコード5:オブジェクト指向との組み合わせ

Javaらしい使い方として、オブジェクトのメソッドを各要素で呼び出すパターンがあるのが一般的です。PersonのリストへforEachを適用し、それぞれのintroduceを呼び出します。

import java.util.Arrays;
import java.util.List;

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void introduce() {
        System.out.println("私の名前は" + name + "で、年齢は" + age + "歳です。");
    }
}

public class Main {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 40),
            new Person("Charlie", 50)
        );

        // オブジェクトのメソッドを呼び出し
        people.forEach(person -> person.introduce());
    }
}

結果: 期待される出力は、3人分の自己紹介文です。

私の名前はAliceで、年齢は30歳です。
私の名前はBobで、年齢は40歳です。
私の名前はCharlieで、年齢は50歳です。

結果: これは期待される出力例です。各Personオブジェクトのintroduce()が順番に呼ばれます。

このように、処理をオブジェクト側へ寄せると、forEach内のラムダ式は短くなります。メソッド参照を使える場合はpeople.forEach(Person::introduce)のように書くこともできるのが現実的です。

注意点と対処法

forEachは短く書けますが、注意点を知らずに使うと、例外、順序、共有状態、途中終了の扱いでつまずきます。特に押さえたいのは、nullの扱い、ラムダ式の例外、並列処理、コレクション変更です。

nullの扱い

nullのコレクションに対してforEachを呼ぶと、NullPointerExceptionが発生します。メソッド呼び出しの対象が存在しないため、ラムダ式の中身に進む前に失敗すると整理できます。

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> items = null;
        items.forEach(item -> System.out.println(item));
    }
}

結果: 期待される動作は、NullPointerExceptionが発生することです。itemsnullのため、forEachを呼べません。

そのため、入力値がnullになる可能性がある処理では、呼び出し前に判定します。空のリストを返す設計にできるなら、呼び出し側の分岐を減らせます。

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> items = null;
        if (items != null) {
            items.forEach(item -> System.out.println(item));
        }
    }
}

結果: 期待される出力はありません。itemsnullなので、ifの内部処理は実行されません。

エラーハンドリング

ラムダ式の中で例外が起きる処理を扱う場合、try-catchの場所を決める必要があります。文字列を数値へ変換する例では、Integer.parseIntが変換できない文字列に対してNumberFormatExceptionを投げます。

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> numbers = Arrays.asList("1", "2", "three", "4");
        numbers.forEach(number -> {
            try {
                int converted = Integer.parseInt(number);
                System.out.println(converted);
            } catch (NumberFormatException e) {
                System.out.println(number + " は数値に変換できません。");
            }
        });
    }
}

結果: 期待される出力は12three は数値に変換できません。4です。

この対処法では、変換失敗を1要素分のエラーとして扱い、ほかの要素の処理を続けます。逆に、1件でも失敗したら全体を止めたい場合は、ラムダ式の外側で設計を分けるほうが明確です。

スレッドセーフについて

一般的なArrayListHashMapは、複数スレッドから同時に変更される前提ではありません。そのため、parallelStreamや非同期処理と組み合わせると、共有状態の更新で問題が起きる可能性があります。

このときは、ConcurrentHashMapCopyOnWriteArrayListAtomicIntegerのような並行処理向けAPIを検討すると理解できます。ただし、スレッドセーフな型を使えば常に速くなるわけではなく、処理の粒度と共有状態の有無が判断材料になります。

パフォーマンス面での注意

大量データを扱う場合、forEachそのものより、ラムダ式の中で行う処理がボトルネックになることがあります。特に標準出力、ファイルI/O、データベースアクセスは、ループ構文を変えても根本的な待ち時間が残りますが、覚えておくと役立つでしょう。

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 1000000).parallel().forEach(num -> {
            // 何らかの処理
        });
    }
}

結果: 期待される表示はありません。parallel()で並列ストリームを作り、各数値に対する処理枠だけを示しています。

ただし、並列化は順序と共有状態の注意点を増やします。順番が必要な処理ではforEachOrderedを検討し、状態を更新する処理では競合が起きない設計にすると覚えるとよいでしょう。

ℹ️ 補足: 公式ドキュメントでは、HashMapが順序を保証しないことも明記されています。順序が意味を持つ出力では、Oracle Java SE 17 HashMapの仕様も確認しておくと安全です。

カスタマイズ方法

forEachのカスタマイズでは、共通処理を独自メソッドにまとめる方法と、既存ライブラリが提供するコールバック形式へ理解を広げる方法があります。どちらもConsumerやラムダ式の考え方を使うため、基本の使い方がそのまま土台になると考えられます。

独自のforEachメソッドを作成する

独自のcustomForEachを作ると、各要素の処理前にログ出力や検証を差し込めます。引数にConsumer<T>を受け取る設計にすると、呼び出し側はラムダ式で任意の処理を渡せます。

import java.util.List;
import java.util.function.Consumer;

public class CustomForEach {
    public static <T> void customForEach(List<T> list, Consumer<T> action) {
        for (T item : list) {
            // ここで独自の処理を追加
            System.out.println("Processing: " + item);
            action.accept(item);
        }
    }

    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");
        customForEach(names, name -> System.out.println("Hello, " + name + "!"));
    }
}

結果: 期待される出力は、各名前の前にProcessing:が表示され、その後に挨拶文が表示される内容です。

Processing: Alice
Hello, Alice!
Processing: Bob
Hello, Bob!
Processing: Charlie
Hello, Charlie!

結果: これは期待される出力例です。customForEachが共通の前処理を行い、渡されたactionが要素ごとの処理を担当します。

このカスタマイズは、処理の入口を統一したいときに役立ちます。一方、独自メソッドを増やしすぎると標準APIから離れるため、チーム内で意図が共有できる範囲に留めるのが現実的です。

既存のライブラリやフレームワークとの連携

Javaのライブラリには、forEachと似た発想でコールバックを受け取るAPIがあると言えるでしょう。Spring FrameworkのJdbcTemplate.queryも、行ごとの処理をラムダ式で渡す構成にできます。

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

public class UserRepository {
    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void printAllUsers() {
        String sql = "SELECT * FROM users";

        // JdbcTemplateと連携
        jdbcTemplate.query(sql, (RowMapper<Object>) (rs, rowNum) -> {
            System.out.println("User ID: " + rs.getInt("id"));
            System.out.println("User name: " + rs.getString("name"));
            return null;
        });
    }
}

結果: 期待される出力は、usersテーブルから取得された各行のIDと名前です。実際の表示内容はデータベース内のレコードに依存します。

この例では、データベース接続、テーブル定義、Springの設定が前提になります。標準Javaだけで完結するサンプルコードではないため、導入時は依存関係と設定ファイルを確認するのが基本です。

同様に、Webアプリケーションやバッチ処理では、ライブラリ側が用意したコールバックAPIにラムダ式を渡す場面が増えます。forEachの理解は、そうしたAPIの読み解きにもつながります。

まとめ

JavaのforEachは、コレクションの要素へ同じ処理を適用するループ表現として、短く読みやすいコードを書きたい場面に合いるのが目安です。特に出力、ログ、オブジェクトのメソッド呼び出しでは、通常のfor文より処理の目的を前面に出せます。

一方、値の変換や新しいリスト作成が目的なら、filtermapcollectを組み合わせる使い方が自然です。その使い分けができると、サンプルコードを写す段階から抜け出し、処理の目的に合ったJavaコードへ近づけます。

注意点として、nullのコレクション、HashMapの順序、ラムダ式内の例外、外部変数の扱い、並列処理の共有状態は見落としやすい部分です。これらを押さえると、forEachの使い方を安全に広げられますし、ここを基本と考えるとよいでしょう。

応用例やカスタマイズまで理解すると、コーディングスピードを上げながら、読み手にも意図が伝わるループ処理を書きやすくなります。Javaの学習では、for文、拡張for文、forEachStreamを比較しながら使うと整理しやすくなります。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。