Javaプログラミングにおいて、繰り返し処理(ループ)はプログラムの論理構造を支える最も重要な要素の一つです。

同じ処理を何度も記述する手間を省くだけでなく、大量のデータを効率的に処理し、動的なプログラムを実現するために欠かせません。

Javaには古くから使われているfor文やwhile文に加えて、Java 8以降で導入されたStream APIによる宣言的な記述方法など、多様な選択肢が用意されています。

本記事では、Javaにおける繰り返し処理の基礎から応用までを網羅的に解説します。

それぞれの構文の使い方はもちろん、パフォーマンスや可読性の観点からどのように使い分けるべきか、実務で役立つ知識を詳しく紐解いていきましょう。

Javaにおける繰り返し処理の基本

Javaの繰り返し処理は、大きく分けて「回数指定型」「条件依存型」「コレクション・配列特化型」の3つのカテゴリに分類できます。

開発者は処理したいデータ構造や終了条件に応じて、最適な構文を選択する必要があります。

基本的なfor文(カウンタ変数利用)

最も伝統的で汎用性が高いのが、カウンタ変数を用いたfor文です。

指定した回数だけ処理を繰り返したい場合や、配列のインデックスを直接操作したい場合に適しています。

基本構文と動作原理

for文は、「初期化式」「継続条件式」「更新式」の3つの要素で構成されます。

Java
for (初期化式; 継続条件式; 更新式) {
    // 繰り返し実行する処理
}
  1. 初期化式:ループ開始時に一度だけ実行されます。通常はカウンタ変数の宣言と初期化を行います。
  2. 継続条件式:各ループの開始前に評価されます。この式が true の間、処理が継続されます。
  3. 更新式:ループ内の処理が終わるたびに実行されます。カウンタ変数のインクリメント(加算)などを行います。

具体的なコード例

以下の例では、0から4までの数字を順に出力します。

Java
public class ForExample {
    public static void main(String[] args) {
        // 0から開始し、5未満の間繰り返し、1ずつ加算する
        for (int i = 0; i < 5; i++) {
            System.out.println("現在のインデックス: " + i);
        }
    }
}
実行結果
現在のインデックス: 0
現在のインデックス: 1
現在のインデックス: 2
現在のインデックス: 3
現在のインデックス: 4

拡張for文(for-each文)

Java 5で導入された拡張for文は、配列や Iterable インターフェースを実装したコレクション(List, Setなど)の全要素を順番に処理するのに最適化されています。

特徴とメリット

拡張for文の最大のメリットは、インデックス管理が不要である点です。

これにより、配列の範囲外参照(ArrayIndexOutOfBoundsException)のリスクを排除し、コードの可読性を劇的に向上させることができます。

具体的なコード例

List内の要素をすべて出力する例を見てみましょう。

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

public class EnhancedForExample {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");

        // 要素を一つずつ取り出して変数 fruit に代入
        for (String fruit : fruits) {
            System.out.println("フルーツ名: " + fruit);
        }
    }
}
実行結果
フルーツ名: Apple
フルーツ名: Banana
フルーツ名: Cherry

この構文は非常に簡潔ですが、「現在の要素が何番目か」というインデックス情報が必要な場合や、ループの中で要素を削除・変更する場合には不向きです。

その場合は基本のfor文や、後述する Iterator を使用します。

条件によって繰り返すwhile文とdo-while文

繰り返し回数が事前に決まっておらず、特定の条件を満たしている間だけ処理を続けたい場合には、while文やdo-while文を使用します。

while文の基礎

while文は、処理を開始する前に条件判定を行います。

条件が最初から false であれば、一度も実行されない可能性があるのが特徴です。

Java
public class WhileExample {
    public static void main(String[] args) {
        int count = 0;
        
        // countが3未満の間繰り返す
        while (count < 3) {
            System.out.println("countの値: " + count);
            count++; // これを忘れると無限ループになるので注意
        }
    }
}
実行結果
countの値: 0
countの値: 1
countの値: 2

while文を使用する際の注意点は、無限ループです。

条件式がいつまでも false にならない場合、プログラムは停止しなくなります。

ループ内で必ず条件に影響を与える変数を更新するように設計しましょう。

do-while文の基礎

do-while文は、処理を実行したに条件判定を行います。

そのため、条件に関わらず必ず最低1回は処理が実行されることが保証されます。

Java
public class DoWhileExample {
    public static void main(String[] args) {
        int count = 10;

        // 条件は「count < 5」だが、doブロックは必ず1回実行される
        do {
            System.out.println("この行は必ず一度は実行されます。count: " + count);
            count++;
        } while (count < 5);
    }
}
実行結果
この行は必ず一度は実行されます。count: 10

ユーザー入力を受け取り、その入力内容をチェックして再度入力を促すような処理(バリデーションを伴う入力処理)において、do-while文は非常に有効です。

繰り返し処理の制御:breakとcontinue

ループの途中で処理を中断したり、特定の回だけ処理をスキップしたりするための制御構文として、breakcontinue が用意されています。

break文:ループの強制終了

break文は、実行された瞬間に最も内側のループを完全に終了させます。

特定の条件を満たした時点で探索を終了したい場合などに使用します。

Java
public class BreakExample {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            if (i == 5) {
                System.out.println("5で見つかったので終了します。");
                break; // ループを抜ける
            }
            System.out.println("処理中: " + i);
        }
    }
}
実行結果
処理中: 1
処理中: 2
処理中: 3
処理中: 4
5で見つかったので終了します。

continue文:現在の回のスキップ

continue文は、ループの残りの処理をスキップし、次の繰り返し(更新式や条件判定)へ直ちに移動します。

特定のデータだけを除外して処理を続けたい場合に便利です。

Java
public class ContinueExample {
    public static void main(String[] args) {
        for (int i = 1; i <= 5; i++) {
            if (i % 2 == 0) {
                continue; // 偶数の場合はこれ以降の処理をスキップ
            }
            System.out.println("奇数です: " + i);
        }
    }
}
実行結果
奇数です: 1
奇数です: 3
奇数です: 5

ラベル付きbreak/continue

ネスト(入れ子)構造になったループにおいて、内側のループから一気に外側のループを抜けたい場合があります。

その際に使用するのがラベルです。

Java
public class LabelExample {
    public static void main(String[] args) {
        outerLoop: // ラベルの定義
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (i * j > 4) {
                    System.out.println("条件到達 (" + i + "," + j + ")。全てのループを終了します。");
                    break outerLoop; // 指定したラベルのループを抜ける
                }
                System.out.println("i=" + i + ", j=" + j);
            }
        }
    }
}
実行結果
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
i=2, j=2
条件到達 (2,3)。全てのループを終了します。

ラベルを使用すると複雑なフローを制御できますが、多用するとコードの可読性を損なう恐れがあります。

メソッドを分割して return で抜けるなど、よりシンプルな構造にできないか検討することも大切です。

Stream APIによる現代的な繰り返し処理

Java 8で導入されたStream APIは、従来の命令的なループ(どのように処理するか)から、宣言的な記述(何をしたいか)へのパラダイムシフトをもたらしました。

Stream APIの基本構造

Stream APIは、コレクションなどのデータソースに対して「フィルタリング」「変換」「集計」などの操作をパイプラインのように連結して記述します。

forEachメソッド

最も単純な繰り返しは、forEachメソッドを使用する方法です。

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

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

        // ラムダ式を用いた繰り返し処理
        names.stream().forEach(name -> System.out.println("こんにちは、" + name + "さん"));
        
        // メソッド参照を用いた、さらに簡潔な記述
        // names.forEach(System.out::println);
    }
}

Stream APIを使うメリット

Stream APIを利用する主なメリットは以下の通りです。

可読性の向上

何を行っているかが一目でわかる(フィルタリング、マッピングなど)。

副作用の抑制

関数型プログラミングの考え方を取り入れることで、バグの混入を防ぎやすい。

並列処理の容易化

parallelStream() を使うだけで、マルチコアCPUを活かした並列処理が容易に実装できる。

フィルタリングと加工の例

単なる繰り返しではなく、「特定の条件に合致するものだけを加工してリスト化する」といった処理で真価を発揮します。

Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

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

        // 偶数だけを抽出し、それぞれを10倍にして新しいリストを作成
        List<Integer> processedNumbers = numbers.stream()
                .filter(n -> n % 2 == 0)      // 中間操作1: 偶数のみ抽出
                .map(n -> n * 10)             // 中間操作2: 10倍にする
                .collect(Collectors.toList()); // 終端操作: リストに変換

        System.out.println(processedNumbers);
    }
}
実行結果
[20, 40, 60, 80, 100]

Stream API使用時の注意点

Stream APIは非常に強力ですが、万能ではありません。

例外処理(チェック例外)との相性が悪い

ラムダ式内での例外ハンドリングはコードを複雑にします。

デバッグの難易度

従来のループと比べてスタックトレースが複雑になりやすく、ブレークポイントでのステップ実行も直感的でない場合があります。

オーバーヘッド

非常に小さなデータセットの場合、従来の for 文の方が高速な場合があります。

各繰り返し処理の使い分けガイド

どの構文を使うべきか迷った際は、以下の基準を参考にしてください。

1. 基本のfor文を使うべきケース

  • ループの途中でインデックス(iなど)を利用する必要がある。
  • 配列の要素を逆順に処理したい。
  • 複数の配列を同じインデックスで同時に操作したい。
  • 極限までパフォーマンスを追求しなければならない(特にプリミティブ型配列の処理など)。

2. 拡張for文(for-each)を使うべきケース

  • コレクションや配列の全要素を単に順番に処理したい。
  • インデックスを意識する必要がない。
  • コードの簡潔さと安全性を最優先したい。

3. while / do-while文を使うべきケース

  • 繰り返し回数が不明で、ある状態になるまで続けたい(例:ファイルの読み込みが終了するまで、有効な入力があるまで)。
  • 条件によって、最初から一度も実行しない可能性があるなら while
  • どんな場合でも最低1回は処理させたいなら do-while

4. Stream APIを使うべきケース

  • フィルタリング、変換、集約などのデータ加工が主な目的である。
  • コレクション操作を関数型スタイルで記述し、宣言的なコードにしたい。
  • 大規模なデータを並列処理させたい。
構文適した用途可読性柔軟性
for回数指定、インデックス操作普通高い
拡張 forコレクション全件処理高い低い
while条件依存の繰り返し普通
Stream API複雑なデータ加工・集計非常に高い非常に高い

繰り返し処理におけるパフォーマンスとベストプラクティス

効率的でバグのないプログラムを書くためには、構文を知っているだけでは不十分です。

実務で意識すべきポイントをいくつか紹介します。

リストのサイズ取得はループの外で行う

古いスタイルの for 文において、条件式の中で毎回リストのサイズを計算させるのは非効率な場合があります。

近年のJVMは最適化(JITコンパイル)が進んでいるため、劇的な差は出にくいですが、意識しておくに越したことはありません。

Java
// 改善前
for (int i = 0; i < list.size(); i++) { ... }

// 改善後(サイズを固定値として保持)
int size = list.size();
for (int i = 0; i < size; i++) { ... }

ループ内での重い処理を避ける

ループの中でデータベースへの問い合わせ(SQL発行)や、巨大なオブジェクトの生成を行うと、パフォーマンスが著しく低下します。

  • SQL:ループの外で一括取得(Bulk Select)できないか検討する。
  • インスタンス生成:可能であればループの外で使い回すか、再利用可能な構造にする。

適切なコレクションの選択

繰り返し処理の速度は、使用しているデータ構造に大きく依存します。

例えば、ArrayList の要素にインデックスでアクセスするのは非常に高速(O(1))ですが、LinkedList でインデックスアクセスを行うと、毎回先頭から要素を辿るため非常に低速(O(n))になります。

LinkedListを処理する場合は、必ず拡張for文かIteratorを使用するようにしましょう。

ConcurrentModificationExceptionへの対策

拡張for文の中で、そのループ対象となっているコレクション自体の要素を追加・削除しようとすると、ConcurrentModificationException という例外が発生します。

Java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s); // ここで例外が発生する可能性がある
    }
}

この問題を回避するには、以下のいずれかの方法を取ります。

  1. Iteratorremove() メソッドを明示的に使用する。
  2. Collection#removeIf() メソッドを使用する(Java 8以降)。
  3. 削除対象を別のリストにメモしておき、ループ終了後に一括削除(removeAll)する。
  4. Stream APIの filter を使って新しいリストを作成する。

まとめ

Javaにおける繰り返し処理は、言語の進化とともに多様化してきました。

伝統的な for 文や while 文は、その柔軟性から依然として低レイヤーな処理や複雑なアルゴリズムの実装に欠かせません。

一方で、日常的なビジネスロジックの開発においては、可読性と安全性を両立できる拡張for文やStream APIの利用が推奨されます。

各手法の特性を正しく理解し、単に「動く」だけでなく、「読みやすく、メンテナンスしやすい」コードを選択することが、プロフェッショナルなJavaエンジニアへの第一歩です。

まずはそれぞれの構文を実際に動かしてみて、その挙動の違いを肌で感じてみてください。

プログラミングの要とも言えるループ処理をマスターすることで、あなたのJava開発効率は飛躍的に向上するはずです。