do whileやwhileなど繰り返しの補足 - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は9章「繰り返し」の補足です。
書籍ではwhileやdo-while構文について簡潔な説明を載せていますが、多めの説明をしています。
また、繰り返し構文にまつわる話題とサンプルを2つ載せています。

繰り返しの構文

while文

while文は「条件が成り立つ間、処理が繰り返される」という構文です。for文から「初期化」と「繰り返し時の処理」を省いたものとも言えます。
構文は次のようになります。

while (繰り返し条件) {
  繰り返す処理
}

ここまで単純だと、最初は使い方のイメージがつかないかもしれません。そこでまずはfor文をwhile文に置き換えてみましょう。

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

これもIntelliJ IDEAが変換してくれます。forに入力カーソルを持っていって[Alt / Opt]+[Enter]を押すとメニューが開くので「Replace with 'while'」を選ぶとfor文がwhile文に置き換えられます。

コードは次のようになります。

int i = 0;
while (i < 5) {
    System.out.println(i);
    i++;
}

i < 5という条件が成り立っている間、変数iの値の表示とi++を繰り返します。i++によって変数iの値は1ずつ増えていくので、そのうちi < 5という条件は成り立たなくなります。そのときに繰り返しが終わって次の処理にいきます。ここでは次の処理はないのでプログラムは止まります。


練習

  1. projava.WhileSampleという名前でクラスを作ってここでのサンプルコードを動かしてみましょう 次のコードをwhile文で書いてみましょう
for (int i = 3; i > 0; i--) {
    System.out.println(i);
}
  1. 次のコードをfor文で書いてみましょう
int i = 1;
while(i <= 10) {
    System.out.println(i);
    i += 2;
}

do while文

もうひとつの繰り返し構文はdo while文です。 do while文は繰り返す処理を1度行って、条件を満たしていればもう一度繰り返すという構文です。

do {
  繰り返す処理
} while (条件);

あまり出番は多くないですが、処理を行ってよい結果がでてなかったらもう一回という場合に使います。分かりやすい例でいえば、通信を行ってエラーになったら一回のようなリトライ処理ですね。

var r = new Random();
int score;
do {
    score = r.nextInt(100);
} while(score < 80);
System.out.println("%d点!".formatted(score));

動かすと84点!のように表示されます。 ここで使っているRandomクラスは疑似乱数を生成するためのクラスで、java.utilパッケージに属しています。「乱数」は予測不能な数を返すものです。「疑似」とついているのは、計算によって生成するので計算式やパラメータさえわかっていれば次の値が予測できるため真の乱数ではないということです。 nextIntメソッドは与えた整数のひとつ下の値までを生成します。ここでは0から99までの数字を生成します。

score = r.nextInt(100);

80点が取れてなかったらもう一回、となっています。

while(score < 80)

while句で使われる変数はループの前までに宣言しておく必要があります。

int score;

練習 1. projava.DoSampleという名前でクラスを作ってここでのサンプルコードを動かしてみましょう。import文が必要になるので気をつけてください。 2. 80点が表示されることがあるかどうか考えてみましょう


「繰り返し」に関する話題

適切なプログラム

練習の4番目について、多くの人が次のようにしたのではないかと思います。

for (var n = 0; n < 10; n++) {
    System.out.println(n + 1);
}

※ 「1から10まで表示するようにしてみましょう」という問題です。

次のようにした人もいるかもしれません。

for (var n = 1; n < 11; n++) {
    System.out.println(n);
}

どちらも動きとして間違っていないので、「正しいプログラム」です。
けれども、「適切なプログラム」は次のようなものになります。

for (var n = 1; n <= 10; n++) {
    System.out.println(n);
}

ここで「適切なプログラム」はやりたいことを表すプログラムのことを指します。動きや性能など他の要素が同じであれば、やりたいことをうまく表すのがいいプログラムです。
この練習問題でやりたいことは「1から10まで表示」です。であれば「1」と「10」が使われているものが「適切」です。

また、for文を使いこなせるようになるには、基本的な形を覚えたあとで、少しずつ形を変えていくことも大事です。ここでは繰り返し条件で<演算子ではなく<=演算子を使うようにしました。

プログラムはコンピュータに指示を与えて処理を行うだけではなく、ほかのプログラマに「このような処理を記述している」と伝えるものでもあります。
ただし、今回のように、これが他よりうまくやりたいことを表していると言えるようなプログラムばかりではありません。だいたい同じだけどどちらがより適切なプログラムか悩むというのも、プログラマの仕事です。

空ループ問題

次のようなコードを実行して、「Hello」を5回表示したいのになぜか1回しか表示されない、ということがあります。

src/main/java/projava/LoopSample.java

public class LoopSample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++); {
            System.out.println("Hello");
        }
    }
}

入力して試しても、うっかり5回表示されてしまう人もいるかもしれません。そのくらいわかりにくいミスです。
よく見るとfor文の直後に、セミコロンが入っています。

for (int i = 0; i < 5; i++); {
                           ^

IntelliJ IDEAでは黄色く強調表示されます。これはなにか警告があるという合図です。このような警告は確認するようにしましょう。

セミコロン(;)を空ブロック({})に置き換えるとわかりやすいと思います。

for (int i = 0; i < 5; i++){} {
    System.out.println("hello");
}

つまり、このコードでは「なにもしない」を5回くりかえして、printlnを1度実行することになります。
もし何もしないループを書きたい場合はこのような空のブロックを書くほうが、意図した空ループであることがわかりやすいでしょう。
空ループが必要な理由をコメントで記述しておくと戸惑いにくいです。

もう少しループの練習

格子の描画

まずは縦線をたくさん引いてみます。 「projava.GridSample」という名前でクラスを作成して次のコードを入力してください。

src/main/java/projava/GridSample.java

package projava;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;

public class GridSample {
    public static void main(String[] args) {
        var image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
        var g = image.createGraphics();
        for (int x = 50; x <= 500; x += 30) {
            g.drawLine(x, 10, x, 370);
        }

        var f = new JFrame("格子");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new JLabel(new ImageIcon(image)));
        f.pack();
        f.setVisible(true);
    }
}

実行すると次のように縦線が引かれます。

このコードでは、600px×400pxの画像領域を用意して、その画像に描画するGraphicsを変数gで扱えるようにしています。

var image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
var g = image.createGraphics();

画像はImageIconを経由してJLabelで表示するようにして、JFrameウィンドウに追加しています。

f.add(new JLabel(new ImageIcon(image)));

ウィンドウの表示などに関する説明は省略するので、「SwingでのGUI」の章を復習してください。

ループの処理としてX座標が50から500まで30pxごとに処理を行います。

for (int x = 50; x <= 500; x += 30) {

始点と終点のX座標が同じで、Y座標だけ変わっているので縦線が引かれます。

g.drawLine(x, 10, x, 370);

このループを次のループで置き換えてみましょう。

for (int y = 10; y <= 370; y += 30) {
    g.drawLine(50, y, 500, y);
}

そうするとここでは横線が引かれます。

for (int y = 10; y <= 370; y += 30) {

練習として、両方あわせて格子にしてみましょう。

モアレ

「projava.Moire」という名前でクラスを作成して、「GridSample」のループ以外の部分をコピーしてください。 クラス名の部分がGridSampleではなくMoireになっているか注意してください。

ループ部分に次のように入力します。

src/main/java/projava/Moire.java

for (int x = 0; x < 600; x += 5) {
    g.drawLine(x, 0, 600 - x, 400);
}

実行すると次のような模様が描かれます。

ここではX座標を0から600まで5ずつ処理を行います。

for (int x = 0; x < 600; x += 5) {

縦線を引くときは始点と終点で同じ座標を使っていましたが、ここでは始点は0から離れていく方向、終点は600から離れていく方向で座標を指定しているので、斜めの線が引かれていきます。

g.drawLine(x, 0, 600 - x, 400);

このとき、描画のドットの関係で模様が現れます。このような描画誤差による模様を モアレ(Moire) といいます。

練習として、左右に線が引かれていない領域があるので、埋めてみてください。