🍇

6.3 繰り返し(for文、while文、continue文、break文など)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

6.3 繰り返し

チャプターの概要

このチャプターでは、制御構造の1つである反復(繰り返し)、すなわちループの実装方法について学びます。
Javaでループを実装するための方法には、for文(古典的なfor文)、拡張for文、while文、do-while文、forEach文といった構文がありますが、このチャプターで順番に説明していきます。この中でforEach文についてはラムダ式を使うため、本コースの続編である『Java Advanced編』にて取り上げます。

6.3.1 for文

for文とは

for文は、あらかじめ指定された回数分、処理を繰り返すための構文です。
for文には、旧来よりある「古典的なfor文」と、Java 5でサポートされた拡張for文があります。昨今のJavaアプリケーション開発では拡張for文を使うケースの方が多いですが、「古典的なfor文」も現役ですし、理解することによる学習効果も見込まれるため、このレッスンにて丁寧に説明します。
まずfor文による繰り返し処理は、フローチャートでは以下のようになります。

【図6-3-1】for文のフローチャート(1)
image.png

フローチャートは、以下のように記述することもあります。

【図6-3-2】for文のフローチャート(2)
image.png

for文の構文は以下のとおりです。

【構文】for文(古典的なfor文)
for (初期化式; 条件式; 変化式) {
    ....繰り返し実行される処理....
}

for文では、forキーワードの後ろに( )を記述します。forキーワードと( )の間のスペースは任意ですが、可読性確保のためにスペースを1つ入れるのが一般的です。
( )の中には、3つの式を;で区切って指定します。
まず最初に指定するのが、初期化式です。この式はfor文で最初の1回だけ評価されるもので、ループの中で参照されるカウンタ(カウンタ変数)を指定します。カウンタ変数は数値型変数である必要がありますが、特別な事情がない限りは、整数を表すプリミティブ型であるint型変数を使用します。
次に指定するのが、条件式です。この式には、カウンタ変数を用いてループの継続可否を判定するための式を指定します。個々のループの最後の時点では、まず変化式が評価され、その次に条件式が評価されます。条件式の評価結果がtrueの場合はループは継続され、逆にfalseになったらループ全体が終了します。
最後に指定するのが、変化式です。この式は、ループ処理が一回終わるたびに評価されるもので、通常はカウンタ変数を増減させるための式を指定します。
( )の後ろには{ }でブロックを記述します。ブロックの中には、ループ内で繰り返し実行させたい任意の処理を実装します。

for文の具体例(1)

それでは、for文によるループ処理を具体的に説明します。
まずループが一回実行されるたびに、コンソールに「繰り返し」というテキストと、カウンタ変数を表示するプログラムです。以下のコードを見てください。

snippet_1 (Main_6_3)
for (int i = 0; i < 10; i++) {
    System.out.println("繰り返し, カウンタ変数 => " + i);
}

for文の( )内を見ていくと、まず最初に指定されたint i = 0が初期化式にあたります。ここではカウンタ変数として、int型変数のiを0で初期化しています。このようにカウンタ変数の名前にはi、そしてループがネストする場合は、j、kを使うのが慣例です。
またカウンタ変数の初期値には任意の値を指定できますが、0から開始する、というのが一般的なコーディング上の規約です。カウンタ変数はあくまでもループ回数を表す変数のため、この変数を何らかの業務仕様の実現に直接的に使うのは控えた方が良いでしょう。
次に指定された条件式i < 10は、「カウンタ変数iの値が10未満の場合はループを継続する」という意味です。1回目のループは当然にしてこの条件を満たしているため、処理が行われます。条件式を記述するとき、このように比較演算子<を使って「ある値未満であること」を指定するのも、コーディングの慣例の一種です。
次に指定された変化式i++は、インクリメント演算子が使われていますので、「ループ内の処理が一回終わるたびに、iの値に1を足す」という意味になります。
変数iの初期値は0なので、1回目のループ処理が終わると、この変数の値は1になります。
変化式の評価後に条件式が評価され、ループの継続可否を判定しますが、条件式は前述したように「カウンタ変数iの値が10未満であること」が指定されているので、2回目のループに入ります。
このようにしてループが繰り返し行われていきますが、果たしてこの処理は何回実行されるのでしょうか。
このコードでは変数iの値は0から始まり、10回目の処理が終わって値が9から10になると条件式であるi < 10がfalseになるため、ループ処理全体が終了します。つまり最終的には、このループ処理は10回繰り返されることになります。
このように「初期化式ではカウンタ変数の初期値を0にし、条件式では比較演算子<を使い、変化式ではインクリメント演算子を使う」というのが、for文を記述するときの一般的なルールです。
このルールに従うと、条件式に指定された値(snippet_1では10)が、繰り返される回数と一致するため、ソースコードとしての保守性も向上します。
このコードを実行すると、コンソールに「繰り返し」というテキストが、0から9までのカウンタ変数と一緒に10回表示されます。

for文の具体例(2)

次にfor文によって1から10までの整数を合計する、という処理を実装してみましょう。以下のコードを見てください。

snippet_2 (Main_6_3)
int sum = 0; //【1】
for (int i = 0; i < 10; i++) {
    sum += i + 1; //【2】
}

この処理では「合計値」を表す変数sumをfor文の外側で宣言しています【1】が、これには理由があります。それは合計値の意味を鑑みると、1つ1つのループにまたがる形で値を保持する必要があるためです。for文の中で宣言してしまうと、ループのたびに初期化されてしまうため目的を果たすことはできません。
次にループの中を見ていくと、変数sumに対してi + 1を足し込んでいます【2】。足し込む値は1から10なので、カウンタ変数である変数iに1を加えた値を作り、それを変数sumに足し込む、という処理です。
実はこのコードは、以下のようにカウンタ変数の初期値や条件式を替えても、同じことを実現できます。

snippet_3 (Main_6_3)
int sum = 0;
for (int i = 1; i <= 10; i++) {
    sum += i;
}

このfor文では、カウンタ変数であるiの初期値を1とし、条件式を「10以下であること」と指定しています。つまり業務仕様を直接カウンタ変数iで表す、というわけです。
ただし前述したようにfor文では「カウンタ変数の初期値は0にする」というのが一般的なルールのため、仕様である「1から10まで」をカウンタ変数によって直接的に表すのは、控えた方が良いでしょう。

for文の具体例(3)

次にfor文のループの中で、配列の各要素を処理するケースを見ていきましょう。チャプター5.1でも取り上げたように、配列はfor文によるループと組み合わせることで威力を発揮します。
ここでは、Alice、Bob、Carolの3人による、TOIECテストの平均スコアを求める処理を考えてみましょう。チャプター5.1のMain_Average_2を再掲載します。

snippet (Main_Average_2)
int[] scores = {650, 570, 700}; //【1】
double sum = 0; //【2】
for (int i = 0; i < scores.length; i++) { //【3】
    sum += scores[i]; //【4】
}
double avg = sum / scores.length;//【5】

まず【1】で配列を初期化します。次に【2】で、「合計値」を表す変数sumをfor文の外側で宣言します。for文【3】の条件式に注目してください。カウンタ変数iに対して配列scoresのlengthプロパティを指定し、「配列のサイズ未満になる」という式を指定します。
次にループ内の処理ですが、【4】のように配列scoresのインデックスにカウンタ変数を指定します。カウンタ変数の初期値は0であり、配列のインデックスも0から始まるため、カウンタ変数iはそのまま配列のインデックスになります。そしてループによって変数iの値が1つ加算されるにしたがって、配列の各要素を順番に取り出すことができるのです。
最後に合計値を配列のサイズで割り、平均値を算出しています【5】。
このプログラムでは、仮に後からDaveとEllenが追加された場合でも配列scoresへのデータ追加の処理【1】は必要ですが、それ以降に登場する配列scoresに対する処理は修正不要のため、データの増減に応じた柔軟な処理が可能になります。

6.3.2 拡張for文

拡張for文とは

拡張for文は、前のレッスンで取り上げた「古典的なfor文」と同じように、あらかじめ指定された回数分、処理を繰り返すための構文です。ただし「古典的なfor文」とは異なり、配列やコレクションを対象[1]としたループ処理に特化しており、カウンタ変数は必要としません。通常のJavaアプリケーション開発では、コレクションに対するループ処理が高い頻度で行われるため、「古典的なfor文」よりも拡張for文を使うケースの方が多いでしょう。
拡張for文の構文は、以下のとおりです。

【構文】拡張for文
for (データ型 変数名 : 配列またはコレクション) {
    ....繰り返し実行される処理....
}

拡張for文では、指定された配列またはコレクションからループが行われるたびに要素を自動的に取り出し、要素が無くなり次第ループが終了します。
forキーワードの後ろに( )を記述し、配列またはコレクションから取り出される要素のデータ型と、ループ内における変数名を指定します。次に:で区切って、ループ対象の配列またはコレクションを指定します。このときfor文に指定された変数のデータ型と、配列またはコレクションに格納された要素のデータ型が不一致だと、コンパイルエラーになります。例えばint型配列をループ対象とするのであれば、for文のデータ型にもintを指定する必要がある、ということです。
( )の後ろには{ }でブロックを記述し、ループ内で繰り返し実行させたい任意の処理を実装します。
このように拡張for文では、配列またはコレクションの要素が自動的に取り出されるためカウンタ変数を必要としませんが、何らかの理由でカウンタ変数が必要な場合は「古典的なfor文」を使用してください。

拡張for文の具体例

次に、拡張for文によるループ処理を具体例をもとに説明します。ここでは、レッスン6.3.1と同じように、Alice、Bob、Carolの3人による、TOIECテストの平均スコアを求める処理を考えてみましょう。以下のコードを見てください。

snippet (Main_Average_3)
int[] scores = {650, 570, 700};
int sum = 0;
for (int score : scores) { //【1】
    sum += score; //【2】
}
int avg = sum / scores.length;

このコードを、カウンタ変数を使った既出のコード(Main_Average_2)と比較すると、かなり簡潔に記述できている点が分かると思います。
拡張for文を見ると、int型の変数scoreを指定しています【1】。このfor文によるループ対象はint型配列のため、ここにはint型の変数を指定する必要があります。このようにすると、配列から各要素(650、570、700)が自動的に取り出され、順に変数scoreに代入されてループ処理が行われます。ここでは変数sumに対して、変数scoreの値を足し込むことによって合計値を算出しています【2】。

6.3.3 while文とdo-while文

while文とは

while文は、指定された条件が真である間、処理を繰り返すための構文です。繰り返し処理を行う点はfor文と同じですが、ループ処理の継続判定に条件式を使う点がwhile文の特徴です。
フローチャートは以下のようになります。

【図6-3-3】while文のフローチャート
image.png

while文の構文は、以下のとおりです。

【構文】while文
while (条件式) {
    ....繰り返し実行される処理....
}

whileキーワードの後ろに( )を記述します。whileキーワードと( )の間のスペースは任意ですが、可読性確保のためにスペースを1つ入れるのが一般的です。
( )の中には、ループを継続するための条件式を指定します。ここに指定された条件式がtrueである限り、ループが繰り返されます。
( )の後ろには{ }でブロックを記述し、ループ内で繰り返し実行させたい任意の処理を実装します。このときループ内の処理によって、条件式に指定された変数に対して何らかの変更を加えます。そしてその結果、条件式の評価結果がfalseになると、以降のループには進まずにwhile文を抜けます。

while文の具体例(1)

それでは、while文によるループ処理を具体的に見ていきましょう。まずコンソールに「繰り返し」というテキストを表示する処理を、10回繰り返すプログラムです。以下のコードを見てください。

snippet_4 (Main_6_3)
int i = 0; //【1】
while (i < 10) { //【2】
    System.out.println("繰り返し, カウンタ変数 => " + i);
    i++; //【3】
}

for文はカウンタ変数によって繰り返しを制御しますが、while文は必ずしもカウンタ変数が必須ではありません。ただしこの処理では、ループの回数が事前に決められているため、カウンタ変数を使います。
まずwhile文の外側【1】でカウンタ変数iを初期化します。
whileキーワード後の条件式【2】では、カウンタ変数iが10未満であることをループの継続条件として指定します。
ループの中を見ていくと、コンソールに「繰り返し」と表示した後、【3】でカウンタ変数iに1を加算しています。ループの最後まで処理が到達すると、次のループに進むかどうかを条件式によって判定します。条件式にはi < 10(=カウンタ変数iが10未満であること)が指定されているので、最終的にこのループは10回行われることになります。
このようにwhile文では、for文と同じようにカウンタ変数を用いた繰り返し処理が可能です。ただし事前に繰り返し回数が決まっている場合は、for文の方が用途に合っていると言えるでしょう。

while文の具体例(2)

while文は、for文のように「あらかじめ繰り返し回数が決まっているループ」というよりは、「ループ内で変数に何らかの変化を加え、それが条件式を満たさなくなった(評価結果がfalseになった)時点で、ループを終了する」といった処理に使用します。
それではここで「5を底とする指数演算を複数回行い、10000を超えた時点でその値を表示する」という処理を実装してみましょう。

snippet_5 (Main_6_3)
int total = 1; //【1】
while (total < 10000) { //【2】
    total = total * 5; //【3】
}
System.out.println(total); //【4】

この処理は、事前に何回繰り返すことになるかが分からないため、カウンタ変数は使用できません。
まず【1】で、演算結果を格納するためのint型変数totalを初期化します。次にwhile文ですが、「変数totalの値が10000未満であること」を条件式に指定します【2】。ループの中では、演算結果に5を掛けることで指数演算を行います【3】。演算結果が10000以上になると、指定された条件式に基づいて次のループには進まずループ全体が終了します。そしてその時点の変数totalを、コンソールに表示します【4】。

while文の具体例(3)

while文の使用方法の1つに、ループの継続判定を行うための論理値を返すAPI(メソッド)と組み合わせて使う、というものがあります。特にJava SEのクラスライブラリでは、ファイル入出力処理などにおいてこういったパターンが多く見られます。
ここでは、指定された区切り文字によって文字列を分割するためのStringTokenizerクラス[2]を取り上げ、while文がどのように使われるのかを紹介します。以下のコードを見てください。

snippet_6 (Main_6_3)
String str = "foo,bar,baz,qux";
StringTokenizer st = new StringTokenizer(str, ",");
while (st.hasMoreTokens()) { // 【1】
    String name = st.nextToken();
    System.out.println(name);
}

このコードを実行すると、文字列"foo,bar,baz,qux"がカンマによって分割され、"foo"、"bar"、"baz"、"qux"という順に、コンソールに表示されます。
while文の条件式【1】では、StringTokenizerクラスのhasMoreTokens()というAPIを呼び出していますが、これは「次の分割文字列が存在する場合はtrueを返す」という仕様になっています。従って"foo"、"bar"、"baz"、"qux"という順に文字列が処理され、次の文字列が無くなると条件式がfalseになるため、ループ全体が終了します。

do-while文とは

do-while文は、while文と同じように、指定された条件が真である間、処理を繰り返すための構文です。while文との違いは、while文が条件式をループの最初に評価するのに対して、do-while文では条件式をループの最後に評価します。従って最低でも1回はループが行われる、というのがdo-while文の特徴です。
フローチャートは以下のようになります。

【図6-3-4】do-while文のフローチャート
image.png

do-while文の構文は、以下のとおりです。

【構文】do-while文
do {
    ....繰り返し実行される処理....
} while (条件式);

doキーワードの後ろに{ }でブロックを記述し、ループ内で繰り返し実行させたい任意の処理を実装します。
そしてブロックの後ろにwhileキーワードと( )を記述し、ループを継続するための条件式を指定します。
なおdo-while文の最後には、命令文の終端を表す;が必要になる点に注意してください。

do-while文の具体例

具体例として既出のsnippet_5と同じ処理、すなわち「5を底とする指数演算を複数回行い、10000を超えた時点でその値を表示する」という処理をdo-while文で実装してみましょう。以下のコードを見てください。

snippet_7 (Main_6_3)
int total = 1;
do {
    total = total * 5;
} while (total < 10000);
System.out.println(total);

このコードを実行すると、snippet_5とまったく同じ結果が得られます。

while文とdo-while文の使い分け

while文とdo-while文の違いは、前述したように、最低でも1回はループが行われるかどうかです。大半のケースにおいて、両者の挙動は同じになります。実際のJavaアプリケーションの開発では、両者を使い分ける必然性はほとんどありません。従って、特別な理由がない限りは一般的にもよく用いられるwhile文のみを使用し、do-while文は補助的な理解に留めておけば十分でしょう。

6.3.4 ループの制御と無限ループ

ループの制御とは

これまで説明してきたように、for文やwhile文では、あらかじめ決められた条件に従って繰り返し処理が行われます。ただしループ内での処理に応じて柔軟にループを制御したいケースがあります。つまりループ内で何らかの判定を行い、その結果に応じて残りの処理をスキップして次のループに進んだり、ループ全体を終了させたりする、といったケースです。
このようにループを制御するためには、continue文やbreak文を使用します。

continue文による後続処理のスキップ

continue文は、ループ内における何らかの判定結果に応じて後続処理をスキップし、直ちに次のループに進むために使用します。
フローチャートで表すと、以下のようになります。

【図6-3-5】フローチャート(continue文による後続処理のスキップ)
image.png

continue文の挙動を具体例によって説明します。ここではfor文によって「1から100までの整数のうち、3の倍数のみを足し込んでいく」という処理を実装します。

snippet_8 (Main_6_3)
int sum = 0;
for (int i = 0; i < 100; i++) {
    int num = i + 1; // 加算する数
    if (num % 3 != 0) { //【1】
        continue; //【2】
    }
    sum += i + 1; //【3】
}

【1】のように、if文によって「加算する整数が3の倍数以外か?」という判定処理を行い、trueの場合はcontinue文【2】を呼び出します。このようにすると「加算する整数が3の倍数以外のとき」は直ちに次のループに進み、「加算する整数が3の倍数のとき」に限って加算処理【3】が行われます。
なおこれと同じ処理は、continue文を使わずとも以下のようにして実現可能です。

snippet_9 (Main_6_3)
int sum = 0;
for (int i = 0; i < 100; i++) {
    int num = i + 1;
    if (num % 3 == 0) {
        sum += i + 1;  // 3の倍数のときに加算
    }
}

どちらでも同じことを実現できますが、仮に3の倍数のときのみに行いたい処理が、加算の他にもある場合はどうでしょうか。このコードでは、その分ifブロック内の記述が肥大化していきます。その点では、snippet_8のように「3の倍数以外のとき」という条件で分岐を作り、continue文によって直ちに次のループに進んでしまった方が、コードとしての見通しは良くなります。

break文によるループ全体の終了

break文は、ループ内における何らかの判定結果に応じてループ処理を中断し、ループ全体を終了させるために使用します。
フローチャートで表すと、以下のようになります。

【図6-3-6】フローチャート(break文によるループの終了)
image.png

break文の挙動を具体例によって説明します。ここではboolean型変数を宣言し、for文による配列の繰り返し処理で「3の倍数が1つでもあったらtrueに、それ以外の場合はfalseにする」という処理を実装します。

snippet_10 (Main_6_3)
int[] nums = {8, 5, 10, 12, 1, 9}; //【1】
boolean flag = false; //【2】
for (int i = 0; i < 10; i++) {
    if (nums[i] % 3 == 0) { //【3】
        flag = true; //【4】
        break; //【5】
    }
}

まず【1】で整数の配列を作り、【2】でboolean型変数flagを初期化します。
for文を見ていくと、配列の各要素を取り出し、それが3の倍数かどうかを判定しています【3】。そして「3の倍数」の条件分岐の中で、変数flagをtrueにします【4】。ここで、この処理の仕様は「3の倍数が1つでもあったらtrueにする」というものであることを思い出してください。配列の各要素を見ると、4番目の要素に12が登場し、この時点でtrueであることが確定するため、これ以上ループを継続する必要はありません。このような場合は、break文【5】を記述することによって、ループ全体を終了させます。仮に配列の中に一つも3の倍数がなかった場合は、変数flagはtrueにならないため、初期値であるfalseのままになります。

無限ループ

無限ループとはその名のとおり、際限なく繰り返されるループのことです。無限ループは、開発者が意図的に構築するケースと、プログラムの不具合によって意図せず発生するケースがあります。意図的に構築するケースというのは、具体的には、常駐プログラムやサーバーサイドで稼働するアプリケーションなどです。
コードとしては、以下のようなイメージです。

snippet
while (論理値を返すメソッド呼び出し) {
    // 繰り返される
}

このようにwhile文の条件式に、ループの継続判定を行うための論理値を返すメソッド呼び出しを記述します。するとこのプログラムは、このメソッド呼び出しの結果がtrueである限り、際限なく処理が繰り返されます。当該メソッドからfalseが返されると、ループが終了します。

一方、開発者の意図に反して、条件式がいつまで経ってもfalseにならないような処理の場合も、無限ループが発生します。while文のループでは、意図しない無限ループが発生しないように十分に注意してください。

6.3.5 ループのネスト

ループのネスト

for文やwhile文による繰り返し処理では、ループがネストする(入れ子構造になる)ケースがあります。その典型が、多次元のデータを扱うケースです。
それではループがネストする題材として、学科テストの例を取り上げます。具体的には「Alice、Bob、Carol、Daveの4人の国語、算数、理科、社会の4科目の平均点のうち、最も高い平均点を求める」という処理を考えてみましょう。4人の各科目の得点は、二次元配列によって与えられるものとします。以下のコードを見てください。

snippet (Main_NestedLoop_1)
int[][] scoreTable = {{80, 90, 100, 85}, {75, 70, 75, 60}, {95, 65, 75, 90},
        {90, 100, 100, 85}}; //【1】
double maxAvg = 0; //【2】
for (int i = 0; i < scoreTable.length; i++) { //【3】
    double sum = 0;
    for (int j = 0; j < scoreTable[i].length; j++) { //【4】
        sum += scoreTable[i][j];
    }
    double avg = sum / scoreTable[i].length; //【5】
    if (maxAvg < avg) { //【6】
        maxAvg = avg;
    }
}
System.out.println(maxAvg); //【7】

まず4人の各科目の点数を、二次元配列に格納します【1】。【2】は最も高い平均点を保持するための変数です。
このコードにはfor文が2つあり、ネストしていることに気が付くでしょう。外側のfor文【3】によって、一次元目のループ、すなわちAliceからDaveまでの人のループが行われます。そして内側のfor文【4】では、各人の処理において二次元目のループ、すなわち国語から社会までの科目のループが行われます。内側のループの中で4科目の合算を行ったら、内側のループを抜け、外側のループの中でその平均点を算出します【5】。そしてその結果を、変数maxAvgと比較する【6】ことで、4人の中で最終的に最も高い平均点を変数maxAvgが保持するようにしています。人別の平均点はAliceが88.75、Bobが70、Carolが81.25、Daveが93.75なので、最終的にはDaveの93.75が変数maxAvgに格納され、外側のループを抜けた後、【7】によってコンソールに表示されます。

ループのネストとbreak文

ループがネストするような複雑なケースでは、continue文やbreak文によるループ制御が必要になることがあります。
ここでも前項と同じように、学科テストの例を用いて説明します。今度は「Alice、Bob、Carol、Daveの4人の国語、算数、理科、社会の4科目の得点において、科目は問わないが100点を取ったことのある人を数える」という処理を考えてみましょう。以下のコードを見てください。

snippet (Main_NestedLoop_2)
int[][] scoreTable = {{80, 90, 100, 85}, {75, 70, 75, 60}, {95, 65, 75, 90},
        {90, 100, 100, 85}};
int count = 0; // 100点取得者の数
for (int i = 0; i < scoreTable.length; i++) {
    for (int j = 0; j < scoreTable[i].length; j++) { //【1】
        if (scoreTable[i][j] == 100) {
            count++; //【2】
            break; //【3】
        }
    }
}
System.out.println(count);

このコードにはfor文が2つあり、ループがネストしています。外側のループは、Alice、Bobといった人のループ、内側のループは国語、算数といった科目のループです。内側のfor文【1】では、各科目の点数が100点かどうかを判定し、trueの場合は、100点取得者を数えるための変数countに1を加算します【2】。ひとたび100点取得者としてカウントしたら、それ以上他の科目を判定する必要はありませんので、break文【3】によってループを終了させます。
ここで注意が必要なのは、break文によって終了されるループは、break文が記述された位置のすぐ外にあるループになる、という点です。従ってこのbreak文によって終了するのは科目ループを表す内側のfor文です。
外側のfor文は処理が継続されるため、次の人のループが開始されます。そして最終的には、100点取得者の数をコンソールに表示して処理を終えています。
これをフローチャートで表すと、以下のようになります。

【図6-3-6】ループのネストとbreak文
image.png

ラベルによるループ制御

もう1つ、別の例を示します。今度も同じく学科テストの例ですが、仕様が若干異なり「Alice、Bob、Carol、Daveの4人の国語、算数、理科、社会の4科目の得点において、70点未満の点を取ったことのある人が一人でもいたら、その時点で"追試開催"と表示して処理を終える」という処理を考えてみましょう。以下のコードを見てください。

snippet (Main_NestedLoop_3)
int[][] scoreTable = {{80, 90, 100, 85}, {75, 70, 75, 60}, {95, 65, 75, 90},
    {90, 100, 100, 85}};
PERSON_LOOP : for (int i = 0; i < scoreTable.length; i++) { //【1】
    for (int j = 0; j < scoreTable[i].length; j++) {
        if (scoreTable[i][j] < 70) { //【2】
            System.out.println("追試開催");
            break PERSON_LOOP; //【3】
        }
    }
}

まず注目していただきたいのが、外側のfor文の前に記述された"PERSON_LOOP"という文字列です【1】。これはラベルと呼ばれているもので、for文やwhile文によるループを識別するために使用します。ラベルはforキーワードやwhileキーワードの前に記述し、後ろに:を付与します。なぜこの処理においてラベルが必要になるのかは、この後説明します。
次に内側のfor文を見ると、得点が70点未満かどうかを判定しています【2】。そしてその結果がtrueの場合は、「追試開催」とコンソールに表示し、break文【3】によってループを終了させています。
このとき終了させるループは、外側のfor文、内側のfor文のどちらになるでしょうか。この例は「70点未満の科目が1つでもあった時点で処理を終了させる」という仕様のため、break文によって処理を終了させるのは、外側のfor文になります。そこでbreak文にラベル"PERSON_LOOP"を指定することで、外側のfor文を終了することを明示しています。
このようにcontinue文やbreak文によってネストされたループを制御する処理では、ラベルを使用して、対象となるループを指定しなければならないケースがあります。
上記コードをフローチャートで表すと、以下のようになります。

【図6-3-7】ラベルによるループ制御
image.png

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 「古典的なfor文」による繰り返し処理の方法について。
  2. 拡張for文による繰り返し処理の方法と、「古典的なfor文」との違いについて。
  3. while文による繰り返し処理の方法や、do-while文との違いについて。
  4. continue文やbreak文によるループの制御について。
  5. ループがネストするケースや、ラベルによるループの制御について。
脚注
  1. 拡張for文が利用できるのは、配列以外では、厳密にはjava.lang.Iterableインタフェースを実装したクラスであり、Java SEのコレクションはそれに該当する。拡張for文によるコレクションの一括処理はチャプター18.1で取り上げる。 ↩︎

  2. while文の条件式にループの継続判定ための論理値を返すAPIを指定する分かりやすい例として、ここではStringTokenizerクラスを取り上げたが、このクラス自体は古典的なクラスであり、Strinクラスのsplitメソッドで代用可能。 ↩︎

Discussion