[雑談] 試験の役に立たないJava講座 (7)

ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。

以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。

こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。

ということで今回は第4章、ループ構造とbreak、continue文について少し考えてみたいと思います。

while文

ループ構造を考える場合、最初にfor文から手を付けていくのが定石のようですが、for文はwhile文の拡張的な面が大きいので、while文から始めていきたいと思います。

while文は「while ([条件式]) [文]」という形式で条件式が成立している間、文を繰り返すループ構造となっています。

例えば1から10までの整数を足し合わせるプログラムであれば以下のように書くことができます。

int i = 1; // 足し合わせる数、1で初期化する
int sum = 0; // 足し合わせた結果、最初は0
while (i <= 10) {

  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

  ++i; // iを1増やす

}

whileループは、最初にカッコ内の条件式を検査し、条件が成立していればループ本体の文を実行します。

そして、ループ本体の文を実行したのちに再度条件式を検査して条件が成立していればループ本体の文を再度実行します。

最初の1回を含め、条件が成立していないと判断された時点でループ本体の文は実行されずにwhile文は終了して次の文が実行されます。

上の例では文として中かっこ「{}」でくくったブロックをループ本体の文としていますが、もちろん以下のような1行だけの文でも構いません。

int i = 1; // 足し合わせる数、1で初期化する
int sum = 0; // 足し合わせた結果、最初は0
while (i <= 10)

  sum += i++;

最初の例で++iと書いていたところは趣味の問題ですが、2番目の例でi++としているのはきちんと意味があります (気になる方は演算子の項を参照してください)。

forループ

さて、最初の例に戻ってみると、このプログラムの結果として必要なのはsumという変数だけで、iという変数はループの中だけでしか使われないだろうことが想像できます。

こういった変数をループ変数などと呼んだりします。

ループ変数はループの外に出てこないほうが望ましい (そのうちスコープのところで触れると思います) ので、ループ内に入れ込もうというのがfor文になります。

for文はループ変数の宣言、もしくは初期化式、条件式、ループ変数を更新する式のセットとループ本体の文で構成されます。

先程の例をfor文で書き直すと以下のようになります。

int sum = 0; // 足し合わせた結果、最初は0
for (int i = 1; i <= 10; ++i) {
  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

}

ループ変数に関する操作がforのカッコ内にまとめられるてループ本体の処理と分離されるので構造がわかりやすくなったと思います。

一つ重要な点としてループ変数、上の例のifor文の中でしか有効ではないという点が挙げられます。

このため、以下のようなプログラムはコンパイルエラーとなります。

int sum = 0; // 足し合わせた結果、最初は0
for (int i = 1; i <= 10; ++i) {
  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

}

System.out.println("i: " + i + ", sum: " + sum);

上の例の最後の行System.out.println()の中でループ変数iを参照していますが、すでにfor文の外に出ているので、ループ変数iは無効になりアクセスできないためです。

このようにループ変数をfor文の中だけで有効とする利点として、複数のfor文で同じループ変数が使えるという点が挙げられます。

int sum = 0; // 足し合わせた結果、最初は0
for (int i = 1; i <= 10; ++i) {
  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

}

for (int i = 1; i <= 10; ++i) {

  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

}

上の例のように同じループ変数iを使用したfor文が複数あってもエラーにはなりません。

また、for文は複数のループ変数を扱うことができます。

int sum = 0; // 足し合わせた結果、最初は0
for (int i = 1, j = 1; i <= 10; ++i, ++j) {
  // iが10以下の間、以下を繰り返す

  sum += i + j;

}

ここで注意しなければいけないのは、for文の最初で変数宣言を複数行う場合、すべてのループ変数は同じ型でなければいけないということです。

例えばint i = 1, long l = 1のような書き方はエラーになります。

また、ループ変数宣言における初期化式は必須ではありませんが、初期化していない変数の参照はコンパイルエラーとなりますので、実用上は初期化が必須となります。

更新部分で複数の式をカンマ「,」でつなげて複数書いていますが、これはfor文特有の書き方で、C言語のカンマ演算子のようにどこでも使えるというものではない点に注意が必要です。

ところで、条件式や更新式ではループ変数の操作以外のこともできます。

例えば、こんな書き方もやろうと思えばできます。

int sum = 0;
for (int i = 1; i <= 10; sum += i, ++i) ;

for文の更新部に複数の式がある場合、左から右に順に評価されるので、先の例ではループ本体にあったsum += iが評価された後で++iが評価されるので、先の例と同じ結果となります。

さらにトリッキーな例として、こんなこともできます。

int sum = 0;
for (int i = 1; i <= 10 && (sum += i) > 0; ++i) ;

まあ、こんなことしてもわかりにくいだけで誰も喜ばないので、できるということだけ知っていればいいかと思います。

ところで、for文の初期化部、更新部、条件式は省略することができます。

初期化部と更新部は省略されれば何もしないというだけでわかりやすいと思います。

例えば、先の例であれば以下のような形にできます。

int i = 1; // 足し合わせる数、1で初期化する
int sum = 0; // 足し合わせた結果、最初は0
for (; i <= 10; ) {

  // iが10以下の間、以下を繰り返す

  sum += i; // iをsumに加える

  ++i; // iを1増やす

}

一言で言うと先のwhile文の例と全く同じになりますので、こういった状況でfor文を使用する意味はありません。

例えばループ変数がどこかで自然に初期化されている場合に初期化部を省略するといった使い方が多いかと思います。

そして条件式が省略された場合には条件はいつでも成り立つとみなされ、無限ループとなります。

無限ループからの脱出方法はこの後break文のところで説明します。

さて、無限ループを書きたい場合、for文を使って以下のように書く例がよく示されています。

for (;;) {
  // 無限ループの本体
}

たぶん入力する文字数が少ないから使われているのだと思いますが、自分は以下のようにwhile文を使った方が意図が明確になっていいのではないかと思っています。

while (true) {
  // 無限ループの本体
}

とはいえ、本来的には無限ループが本当に必要か、設計レベルから検討すべきというのが正論ですけど。

do-while文

ループの初期化処理がループ本体とほぼ変わらないといった場合がたまにあります。

と言っても適当な例が見つからないので概念的なプログラムになってしまいますが、こんなものです。

// ループの準備処理
// 準備作業としてループ本体と同じ処理をここで行う
while (条件) {

  // 準備作業と同じループ本体処理

}

このような場合、ループの準備作業とループ本体で同じコードを2回書かなければいけないという無駄があるとともに、ループ本体に修正が必要となった場合に準備作業とループ本体の両方に修正を入れなければならないという面倒があります。

こういった場合のためにJavaではdo-while文を用意しています。

上の例はdo-while文を使用して以下のように書くことができます。

do {
  // ループ本体処理
} while (条件式);

ところでBASICではこれと同じような書き方としてDO-UNTIL文というのがありました。

DO
; ループ本体
UNTIL 終了条件

これは終了条件が成り立った場合に繰り返しをやめるという構文で、WHILE文ではないことが一目でわかるという利点を持っていました。

Javaではたぶん新しいキーワードを導入したくないということもあったのだと思いますが、whileで繰り返し条件を指定する形となっており、while文とdo-while文を見間違える恐れがあります。

このため、while文ではなくdo-while文であることを明示するためにループ本体が1行の場合であっても中かっこを省略せず、かつ中かっこ閉じ「}」を行の先頭に書くようにしています。

do-while文は最初に必ずループ本体を実行してから継続条件の判断を行うので、先のような問題が解消されます。

break文

さて、ちょっと前に無限ループの例がありました。

無限ループを使う場合、脱出する方法が必要となります。

また、ループの内容によってはそれなりの計算を行わないと継続判断ができないといった場合もあります。

そういった場合に使われるのがbreak文となります。

break文を使用するとこんな感じになります。

while (true) {
  // 継続条件を判断するための計算
  if (終了条件)

    break;

  // ループでの処理

}

このように書くと継続条件を判断するための計算を行った後、終了条件が成り立った場合にループが終了し、ループでの処理が行われなくなります。

break文はそれが直接含まれるループだけを脱出します。

このため、ループが入れ子になっている場合に外側のループも一気に終了するには何らかの対策が必要となります。

すぐに思いつくやり方だとフラグを使うというのがあります。

boolean breakFlag = false;
while (true) {
  // 内側ループに入るための準備

  while (true) {

    // 継続条件を判断するための計算

    if (終了条件) {

      breakFlag = true;

      break;

    }

    // 内側ループの本体

  }

  if (breakFlag)

    break;

  // 外側ループの処理

}

このように書けば内側ループで終了条件が検出された場合、breakFlagtrueになっているので、外側ループも終了するようになります。

しかし、このやり方は面倒なので、もうちょっと賢い方法としてラベル付きbreakという書き方が用意されています。

OUTER_LOOP:
while (true) {
  // 内側ループに入るための準備

  while (true) {

    // 継続条件を判断するための計算

    if (終了条件)

      break OUTER_LOOP;

    // 内側ループの本体

  }

  // 外側ループの処理

}

このようにコロン「:」で終わるラベルを一気に脱出したいループの直前に書き、break文に脱出するループにつけられたラベルを記述すると外側ループを含めて一気に脱出することができます。

実際にはラベル付きbreak文はループ以外のどこでも使えてしまううえにラベルはさまざまな場所につけることができてしまうので、ループからの脱出だけでなくいわゆるgo to文のようにも使えてしまいます。

が、go toを使うとプログラムの理解が非常に難しくなるので使うべきではないと私は考えています。

continue文

素数を発見する方法の一つにエラトステネスのふるいがあります。

これはテーブルを一つ用意して、2から順に倍数になっているところをマークしていって、一通りのチェックが済んだところでマークされていないところが素数として残るというものです。

これを簡単にプログラミングすると以下のような感じになります。

// 100までをマークするためのテーブルを用意する
boolean table[] = new boolean[101];
// テーブルを初期化 (trueでマークする)

for (i = 2; i < 100; ++i)

  table[i] = true;

// 2から100までチェックする

for (i = 2; i < 100; ++i) {

  // iの倍数のところをfalseでマークする

  for (j = i + i; j < 100; j += i)

    table[j] = false;

}

// tableの素数のところがtrueで残る

単純に二重ループで書いてみましたが、よく考えるとすでに素数でないことがわかっている、言い換えるとtablefalseが設定されている場合には倍数をマークする必要がないので、無駄な処理が行われていることがわかります。

では先の例を直してみます。

// 100までをマークするためのテーブルを用意する
boolean table[] = new boolean[101];
// テーブルを初期化 (trueでマークする)

for (i = 2; i < 100; ++i)

  table[i] = true;

// 2から100までチェックする

for (i = 2; i < 100; ++i) {

  // iの倍数のところをfalseでマークする

  if (table[i]) {

    for (j = i + i; j < 100; j += i)

      table[j] = false;

  }

}

// tableの素数のところがtrueで残る

これで素数でないことがわかっている値の処理は行われなくなりました。

ところで、書き換えたプログラムはループ処理の残り部分を飛ばしているだけなので、continue文を使って以下のようにも書けます。

// 100までをマークするためのテーブルを用意する
boolean table[] = new boolean[101];
// テーブルを初期化 (trueでマークする)

for (i = 2; i < 100; ++i)

  table[i] = true;

// 2から100までチェックする

for (i = 2; i < 100; ++i) {

  // iの倍数のところをfalseでマークする

  if (!table[i])

    continue;

  for (j = i + i; j < 100; j += i)

    table[j] = false;

}

// tableの素数のところがtrueで残る

前のプログラム例が「素数だったら処理をするif (table[i])」だったのに対して、上のプログラムは「素数でなかったら処理を飛ばすif (!table[i])」となっています。

どちらも意味的には変わらないので、どちらが論理として理解しやすいかで選択することになると思います。

最初の例は「実行する条件」に着目しているわけですし、2番目の例は「実行しない条件」に着目しているので、例えばどちらが例外的な事象なのかと言った観点で判断するということです。

あるいは「残りを飛ばす」というイメージが適切かというのも観点になると思います。

以上、ループ構造とbreak文、continue文について考えてみました。

0コメント

  • 1000 / 1000