1. 導入
まず質問から始めましょう。なぜそもそも一つのループを別のループの中に入れる必要があるのでしょうか?データや課題は一次元の列ではなく、たとえば表、グリッド、あるいは多次元構造として整理されていることがよくあります。たとえば、画面に乗法表を表示したい、2 次元配列を走査したい、すべての要素の組の交差を数え直したい、という場合です。このような場面ではループが 1 つでは明らかに足りず、ループの中にループが必要になります。
プログラミングにおけるネストしたループは、2 つの目覚まし時計のようなものです。外側が鳴り始めると、その内側でさらにもう一つが鳴り、外側が動いている間は毎回鳴り続けます。つまり「外側」の 1 回の反復のあいだに、内側は自分の範囲をすべて回り切り(そして外側の各反復ごとにそれを繰り返します)。
良い例が「時」と「分」です。時は外側のループで 0 から 23 まで、分は内側のループで 0 から 59 までです。外側が 1 つ変化するたびに、内側は自分の値をすべて走り切ります。
2. ネストしたループの構文
Java では、ネストしたループの構文は通常のループと何も変わりません。単に一方のループの本体の中にもう一方のループを書くだけです。for と while の例を見てみましょう:
// 外側の for ループ
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++) // 内側の for ループ
{
System.out.print(i + "," + j + " ");
}
System.out.println(); // 内側のループの後に改行
}
ここでは外側のループが変数 i(0 〜 2)を、内側のループが変数 j(0 〜 3)を制御します。各 i の値に対して、内側のループは j == 0 から j == 3 までを完全に走査します。このコードを実行すると、座標の表のような出力が得られます:
0,0 0,1 0,2 0,3
1,0 1,1 1,2 1,3
2,0 2,1 2,2 2,3
while を使った同様の例:
int i = 0;
while (i < 3)
{
int j = 0;
while (j < 4)
{
System.out.print(i + "," + j + " ");
j++;
}
System.out.println();
i++;
}
注意: 外側ループの各周回の中で、内側ループの変数(j)は毎回再初期化する必要があります。そうしないと 1 行しか表示されません。
3. ネストしたループの例
例 1: チェス盤の描画(8x8)
最初の課題として、白黒のマスで表したクラシックなチェス盤を画面に出力してみましょう(「#」を黒、「.」を白とします)。ネストした for ループで実装します:
for (int row = 0; row < 8; row++)
{
for (int col = 0; col < 8; col++)
{
// 行番号と列番号の合計が偶数なら白、そうでなければ黒
if ((row + col) % 2 == 0)
System.out.print("_");
else
System.out.print("#");
}
System.out.println(); // 各行の後で改行
}
結果:
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
重要なポイント: ネストによって、各行(row)に対してすべての列(col)を完全に走査できるようになります。ネストがなければ、盤の構造にはならず、単なる 1 行または 1 列しか得られません。
例 2: 乗法表
ネストしたループの古典的な例です。1〜9 の乗法表を出力してみましょう:
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= 9; j++)
{
System.out.print(i * j + "\t");
}
System.out.println();
}
フォーマット i * j + "\t" によりタブが追加され、表が整って見えるようになります。
結果:
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
...
9 18 27 36 45 54 63 72 81
4. ネストしたループと制御のコツ
break と continue の影響
ここで多くの初心者がつまずきます。内側のループで break や continue を使っても、影響するのはその内側のループだけです。外側のループは何事もなかったかのように処理を続けます。
例: 内側のループだけを早期終了する
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
if (j == 3)
break; // 内側のループだけを抜ける!
System.out.print(i + "," + j + " ");
}
System.out.println();
}
結果:
0,0 0,1 0,2
1,0 1,1 1,2
2,0 2,1 2,2
もし 2 重のループの両方から一度に抜けたい場合(たとえば表で最初の一致を見つけた瞬間に検索を打ち切りたいときなど)は、フラグを使うか、特別な手法(すべてが関数内で行われているなら return など)を用いるのが一般的です。
5. ネストしたループの可視化
ネストしたループの実行順序を「目で見る」のは難しいことがあります。次のフローチャートを見てみましょう:
表形式で確認しましょう。i が 1 から 3、j が 1 から 4 のとき、合計の反復回数はいくつになりますか?
| i | j(各 i に対して走査) | 内側ループの反復回数 |
|---|---|---|
| 1 | 1, 2, 3, 4 | 4 |
| 2 | 1, 2, 3, 4 | 4 |
| 3 | 1, 2, 3, 4 | 4 |
| 合計: 3 × 4 = 12 |
6. ネストしたループでのミスと落とし穴
よくあるミスは、内側ループの変数の初期化を誤ることです。例えば、それを外側ループの外で宣言し、各ステップでリセットしない場合です。その結果、内側ループがまったく実行されなかったり、正しく実行されなかったりします.
int j = 0;
for (int i = 0; i < 3; i++)
{
while (j < 4) // あっ! 最初の反復の後には j がすでに 4 になっている可能性があります。
{
System.out.print(i + "," + j + " ");
j++;
}
System.out.println();
}
この場合、ループは 1 回しか動きません。内側ループの変数は外側の中で初期化することを忘れないでください。
また、うっかり同じ変数名で 2 つのネストしたループを書いてしまうと(for (int i = 0; ...) { for (int i = 0; ...) { ... } })、コンパイラに怒られます。変数はすでに定義されています。
GO TO FULL VERSION