プログラミング/7

出典: CourseWiki

目次

2次元配列

いままでの配列は,添え字を1つだけ指定していました.このような配列を1次元配列と呼びます.

表計算ソフトの表やチェスの盤面のような2次元のものを表現するには,2次元配列が適しています.

同様に3次元配列,4次元配列なども可能ですが,それほど使われません.2次元以上の配列を総称して 多次元配列と呼びます.

2次元配列を作る場合,当然ながら大きさを2つ指定する必要があります.

// オセロ盤.8 × 8の配列
int[][] othello = new int[8][8];
boolean[][] screen = new boolean[40][20];

Javaの2次元配列は実際には配列の配列です.上の screen の例だと, booleanの配列 boolean[20] が,さらに配列になっています.

画像:TwoDimensionArray.png

演習(テキストグラフィックス)

2次元配列を使って,文字を使った簡単なグラフィックスを描いてみましょう.

次のプログラムを見て下さい. 若干長めですが,それぞれのメソッドはたいしたことはしていません. booleanの2次元配列 screen で,点の有無を表現しています (true: あり,false: なし).

public class TextGraphics {
    public static void main(String[] args) {
        // 40x20 の2次元配列を確保 
        boolean[][] screen = new boolean[40][20];
        /*
         * Javaの2次元配列は,実際には配列の配列である.
         * screen[0]〜screen[39]が,intの配列(int[20])を参照している.
         * このため,screen.length は 40 になり,
         * screen[0〜39].length は 20 になる. 
         */
        System.out.println("Screen width: " + screen.length);
        System.out.println("Screen height: " + screen[0].length);
 
        box(screen, 3, 3, 8, 4);
        boxFill(screen, 14, 4, 8, 4);
        circle(screen, 10, 15, 7);
        circleFill(screen, 30, 5, 10);
        display(screen);
    }
 
    // 指定された配列を表示する
    // 左上が原点で,Y軸は下方向に増加する点に注意すること
    private static void display(boolean[][] screen) {
        // 上側のルーラー表示
        System.out.print("  ");
        for (int x = 0; x < screen.length; x++) {
            System.out.print(x % 10);
        }
        System.out.println();
        System.out.print("  ");
        for (int x = 0; x < screen.length; x++) {
            System.out.print("-");
        }
        System.out.println();
        for (int y = 0; y < screen[0].length; y++) {
            // 左側のルーラー表示
            System.out.print(y % 10 + "|");
            // 配列の値に応じて * か . を表示        
            for (int x = 0; x < screen.length; x++) {
                if (screen[x][y]) {
                    System.out.print("*");
                } else {
                    System.out.print(".");
                }
            }
            System.out.println();
        }
    }
 
    // 指定された座標が点を打つ (ただし領域の中に入っているときのみ)
    private static void set(boolean[][] screen, int x, int y) {
        if (inArea(screen, x, y)) {
            screen[x][y] = true;
        }
    }
 
    // 指定された座標が領域内かどうか判定する
    private static boolean inArea(boolean[][] screen, int x, int y) {
        if (x < 0 || screen.length <= x) {
            return false;
        }
        if (y < 0 || screen[0].length <= y) {
            return false;
        }
        return true;
    }
 
    // (x, y)から幅 width,高さ height の塗りつぶした長方形を描画する
    private static void boxFill(boolean[][] screen, int x, int y, int width,
            int height) {
        for (int yy = 0; yy < height; yy++) {
            for (int xx = 0; xx < width; xx++) {
                set(screen, x + xx, y + yy);
            }
        }
    }
 
    // (x, y)から幅 width,高さ height の塗りつぶしていない長方形を描画する
    private static void box(boolean[][] screen, int x, int y, int width,
            int height) {
        // !!!fill in here!!!
    }
 
    // 中心 (cx, cy),半径 radius の塗りつぶした円を描画する
    private static void circleFill(boolean[][] screen, int cx, int cy,
            int radius) {
        // !!!fill in here!!!
    }
 
    // 中心 (cx, cy),半径 radius の塗りつぶしていない円を描画する
    private static void circle(boolean[][] screen, int cx, int cy, int radius) {
        for (int theta = 0; theta < 360; theta++) {
            int x = (int) (Math.cos(Math.toRadians(theta)) * radius);
            int y = (int) (Math.sin(Math.toRadians(theta)) * radius);
            set(screen, cx + x, cy + y);
        }
    }
}

塗りつぶさない長方形を描く box メソッドと,塗りつぶした円を描く circleFill メソッドを書いてください.

実行すると,以下のような表示が得られます. 座標系が普通とちょっと違うことに注意してください.原点が左上,X軸は右方向,Y軸は下方向に増加します.

(等幅フォントでないと表示が乱れます)

Screen width: 40
Screen height: 20
  0123456789012345678901234567890123456789
  ----------------------------------------
0|......................*****************.
1|.....................*******************
2|.....................*******************
3|...********..........*******************
4|...*......*...**************************
5|...*......*...**************************
6|...********...**************************
7|..............**************************
8|..........*..........*******************
9|.......*******.......*******************
0|......**.....**.......*****************.
1|.....**.......**......*****************.
2|....**.........**......***************..
3|....*...........*.......*************...
4|....*...........*.........*********.....
5|...**...........**............*.........
6|....*...........*.......................
7|....*...........*.......................
8|....**.........**.......................
9|.....**.......**........................

displayメソッドは配列の中身を表示します.

setメソッドは,配列の上で点を打つためのものです. 配列からはみ出ないかを inArea メソッドでチェックしています (このチェックをしないと,範囲外の座標が指定された場合に ArrayOutOfBoundsException になります).

circleメソッドでは,三角関数を用いて円を描いています. Math.sin(..),Math.cos(..) はそれぞれ sin, cos を求めるメソッドです.これらのメソッドの引数はラジアン単位なので,Math.toRadians(..) で弧度法(0〜360度)をラジアン(0〜2π)に変換しています.

ヒント:

boxメソッドでは,上下と左右の線を書くだけです.

circleFill メソッドでは三角関数を使う必要はありません.点(x, y)が原点を中心とする半径rの円の中に入るかどうかは, 次の式を満たすかどうかでわかるので,これを応用します.

x^2+y^2\leq r^2

演習(テキストグラフィックス)の回答例

クラス変数

TextGraphicsプログラムでは,mainメソッドで作った screen という配列を,全てのメソッドに引数として 渡していました. このような,クラスの中の多くのメソッドで共通して参照する変数は,(引数として渡さなくても)クラスの全てのメソッドから見えると便利です.

これはJavaではクラス変数(class variable)で実現できます.クラス変数とは,あるクラスの中の全てのメソッドから見える変数です.今までは全ての変数はメソッドの中で定義していましたが,クラス変数はメソッドの外(ただしクラスの中)で定義します.

なお,今まで使っていた,メソッドの中で定義する変数はローカル変数(local variable)あるいは局所変数と呼びます.あるメソッドのローカル変数はそのメソッドの実行中だけ存在しますが,クラス変数はそのクラスが存在する限り存在し続けます.

クラス変数を定義するには,メソッドの外で(ただしクラスの中で),先頭に private static を付けます(実際には private を付けないクラス変数もあるのですが,いまのところこうしておきます).

public class Foo {
  // クラス変数はメソッドの外で定義する.クラスの先頭でまとめて定義することが多い.
  private static int[][] screen = new int[40][20];
  private static int foo;
  private static boolean bar;
  // メソッド定義
  public static void main(String[] args) {
    ..
  }
}

クラス変数の初期化

クラス変数は,main メソッド実行よりも前に初期化されます.

初期化式がついていればその値に初期化されます. 初期化式がついていなければ 0 (booleanの場合はfalse) で初期化されます.(配列の場合は,null という特別な値に初期化されます.null というのはヒープメモリのどこも指していないことを意味する特別な値なのですが,詳しいことは後日やります).

public class Foo {
  private static int foo = 100;  // 初期化式付きのクラス変数
  private static int bar;        // 初期化式なしのクラス変数
  public static void main(String[] args ) {
    System.out.println(foo);     // この時点でfooは初期化されているので100が表示される
    System.out.println(bar);     // 0が表示される.
  }
}

注意

クラス変数は,控えめに使ってください.クラス変数を乱用するとプログラムの見通しが悪くなります. クラス変数がどのように使われているかは,クラス全体を眺めないと分からないからです. (ローカル変数はメソッドの中だけを見ていれば使われ方はわかりました)

  • あるメソッドの中だけでしか使用しない変数はクラス変数にする意味はありません.
  • 寿命が短い変数には使うべきではありません.
    • TextGraphics の例では,screen は寿命が長い変数です.

あるメソッドの中で,クラス変数と同じ変数を定義すると,そのメソッドの中ではそちらが優先されます. 混乱するのでクラス変数と同名の変数は使わないようにしましょう.

public class Foo {
  private static int size = 10;
  private void method() {
    int size = 5;              // 同名の変数定義
    System.out.println(size);  // クラス変数は使われない
  }
}

Eclipse ではクラス変数は普通の変数と見分けがつきやすいように青い斜体で表示されます(デフォルトでは).

final修飾子とマジックナンバー

プログラム実行中に書き変わらない値(実質的には定数)を入れておくような変数は,そのことを明示するため final と宣言することができます.final な変数は,後から値を変更しようとするとコンパイルエラーになります. 特に final なクラス変数は,プログラム中から マジックナンバー(magic number) を減らすためによく使われます. マジックナンバーとは,プログラム中に登場する具体的な値のことです. 例えば,以下のような値はマジックナンバーの例です.

プログラミングでは,マジックナンバーはなるべく避けたほうがよいとされています. プログラムにマジックナンバーを埋め込んでしまうと,以下の問題があります.

  • プログラムをみたときに,その値が何を意味するのか分かりにくい
  • 後から値を変更するときに,間違いやすい
    • 駐車料金の例だと,1000円が1200円に変わったらプログラム中の1000の部分を書き換える必要がありますが,もし1000という値が別の意味でも使われていたら,どの1000を直せば良いのか迷う(そして間違える)ことになります.

このため,マジックナンバーはfinalな変数に入れておいて,プログラム中ではその変数を使うようにします. 変数には意味のある名前を付けられるので,プログラムの意味が分かりやすくなりますし,修正も一ヶ所直せば良いので容易です.

Javaでは,finalな変数は,全て大文字にする習慣があります.

final なクラス変数は,(後から代入できないので)必ず初期化式を使って宣言しないといけません.

public class Bar {
  private static final double PI = 3.14159265;
  private static final double E; // 初期化式がないのでエラー
  public static void foo() {
    PI = 1.0;  // エラー(final変数への代入)
  }
}

クラス変数とfinalを用いたテキストグラフィックス

以下に示します.

クラス変数とfinalを用いたテキストグラフィックス

書式付き出力

桁数を指定して表示したい場合はよくあります(例: "小数点以下3桁").

このようなときは,System.out.printf を使います.

double x = 11.234;
System.out.printf("%4.1f\n", x);   // "  11.2" と表示される

System.out.printfの最初の引数は,書式文字列と呼ばれています. %の後に書式を指定する文字(書式指示子といいます)が続きます. 2つめ以降の引数は,最初の引数で指定した書式指示子で表示する値を指定します. 2つめ以降の引数の数は,書式指示子の数とマッチしている必要があります.

次のサンプルをいろいろ書き換えて実行してみましょう. また,コメントアウトされている部分を実行して例外が発生することを確認して下さい.

書式文字列について詳しくは こちら を参照.

/*
 * System.out.printf のサンプル
 * プログラムと出力をよく見比べてください
 */
public class Printf {
    public static void main(String[] args) {
        double x = 1.2345;
        double y = 12.345;
        double z = 1234.5;
        // "%4.1f" は最低4桁の幅で実数表示,小数点以下1桁という意味
        // \n は改行を表す
        // % 以外の文字はそのまま表示
        // 最低4桁なので,4桁以上必要な場合(z)ははみ出る
        System.out.printf("1234 1234 1234\n");
        System.out.printf("%4.1f %4.1f %4.1f\n", x, y, z);
        System.out.println();
        System.out.printf("12345678 12345678 12345678\n");
        System.out.printf("%8.2f %8.2f %8.2f\n", x, y, z);
        System.out.println();
        
        int i = 1;
        int j = 2;
        long k = 3;
        // "%4d" は最低4桁の幅で整数表示
        System.out.printf("1234 1234 1234\n");
        System.out.printf("%4d %4d %4d\n", i, j, k);        
        // もちろん,%d と %f は混ぜて使える
        System.out.printf("%4d %4d %4.1f\n", i, j, x); 
        // 整数に %f を使ったり,実数に %d を使うとエラー
        // java.util.IllegalFormatConversionException になる  
        //System.out.printf("%4d\n", x);
 
        // % の数と引数の数がマッチしない場合もエラー
        // java.util.MissingFormatArgumentException になる  
        //System.out.printf("%4d %4d\n", i);
    }
}

実行結果

1234 1234 1234
 1.2 12.3 1234.5

12345678 12345678 12345678
    1.23    12.35  1234.50

1234 1234 1234
   1    2    3
   1    2  1.2

System.out.printf の引数の数は固定ではありませんが,これは可変長引数という機能を使っています.

課題7-1(魔方陣)

魔方陣というのは,n × n のマス目(方陣)に 1 から順番に数字を入れたもので(n2まで), 各行,各列,対角線の合計が同じになるような配列をいいます.

下のページにあるように,奇数 x 奇数の魔方陣は簡単なアルゴリズムで作れます.

奇数 x 奇数の魔方陣を表示するプログラムを以下に示します.コメントのせいで長く見えますが, プログラム本体は長くないのでよく読んでください.

displayメソッドだけ中身が抜いてあります.displayメソッドを完成させるのが課題です.

  • System.exit(..) は,プログラムを終了するためのメソッドです.
  • moveメソッドでは,x, y の2つの値を返すために,2要素の配列を返しています.

/*
 * 奇数 x 奇数の魔方陣を生成するプログラム 
 */
public class MagicSquare {
    // 魔方陣の大きさ
    private static int size;
    // 2次元配列
    private static int[][] matrix;
 
    public static void main(String[] args) {
        // 魔方陣のサイズ
        System.out.print("魔方陣のサイズを入力(3以上の奇数): ");
        size = Keyboard.intValue();
        // 奇数でない場合はエラーとして終了
        if (size % 2 != 1 || size < 3) {
            // System.exit(..) はプログラムを終了するためのメソッド.
            // 引数は,プログラムの終了コードで,0 は正常終了,0でない値は異常終了を示す.
            System.out.println("サイズが奇数でないか,3未満です");
            System.exit(1);
        }
        // matrix を確保
        // sizeが分からないと new できないので,ここで new している.
        matrix = new int[size][size];
        // 次に数を入れる座標
        int x, y;
        // 最初の座標を代入する (一番上の段の真ん中)
        // X座標は size / 2 
        // size = 3 の場合,x = 3 / 2 = 1 になる.X方向の添え字は0〜2なので,真ん中は1 でよい.
        x = size / 2;
        // Y座標は0(一番上)
        y = 0;
        // 1から順番に値を詰めていく
        for (int n = 1; n <= size * size; n++) {
            System.out.println("Set " + n + " to (" + x + ", " + y + ")");
            matrix[x][y] = n;
            // 魔方陣の表示
            display();
            // 次の位置を計算する.
            // moveメソッドはxとyの2つの値を返すために,要素数2のintの配列を返している
            int[] xy = move(x, y);
            x = xy[0];    // 配列の0番目は x
            y = xy[1];    // 配列の1番目は y
        }
    }
 
    // 魔方陣を表示する
    private static void display() {
        // !!!fill in here!!!
    }
    
    /*
     * 魔方陣上で,(x, y)の次の場所を計算する.結果は,2要素のint配列で返す.
     * 配列の0番目の要素が x, 1番目の要素がy.
     */
    private static int[] move(int x, int y) {
        /*
         * (x, y)の右上の枠の座標(nx, ny)を求める.
         * このとき,左右,上下の境界は繋がっているものとみなす.
         *            -> X方向
         *      +-----+-----+
         *      |     |nx,ny|
         * |    +-----+-----+
         * v    | x,y |     |
         * Y方向 +-----+-----+
         * 
         * nx は基本的に x + 1 だが,一番右の枠の右は一番左になるので,
         * 剰余(%)を使った次の式で計算する.この形はよく出てくるので暗記するべき.
         */
        int nx = (x + 1) % size;
        /*
         * Y軸は下方向に増えるので,ny は基本的に y - 1 だが,一番上の枠の上は
         * 一番下になるので,これも剰余を使った式で計算する.
         * ny = (y - 1) % size だと,y = 0 のとき ny = -1 % size になるが,
         * size >=2 のとき,-1 % size == -1 になってしまう (-1 ÷ size = 0 余り -1).
         * これだと具合が悪いので size を足してから剰余を計算する.
         * この形も良く出てくるので暗記するべき.
         */
        int ny = (y - 1 + size) % size;
        // 右上の枠(nx, ny)が空いていれば,その座標を返す.
        if (matrix[nx][ny] == 0) {
            // 0番目に nx を,1番目に ny を入れた配列を生成し,return する.
            return new int[]{nx, ny};
            // 上の行は見慣れない書き方だが,
            // int[] xy = {nx, ny};
            // return xy;
            // と等価
        }
        // 右上の枠が空いていない場合,(x, y)の下の枠の座標を返す.
        return new int[]{x, (y + 1) % size};
    }
}

実行結果(サイズ3の場合).

魔方陣のサイズを入力(3以上の奇数): 3
Set 1 to (1, 0)
+--+--+--+
| 0| 1| 0|
+--+--+--+
| 0| 0| 0|
+--+--+--+
| 0| 0| 0|
+--+--+--+
Set 2 to (2, 2)
+--+--+--+
| 0| 1| 0|
+--+--+--+
| 0| 0| 0|
+--+--+--+
| 0| 0| 2|
+--+--+--+
Set 3 to (0, 1)
+--+--+--+
| 0| 1| 0|
+--+--+--+
| 3| 0| 0|
+--+--+--+
| 0| 0| 2|
+--+--+--+
Set 4 to (0, 2)
+--+--+--+
| 0| 1| 0|
+--+--+--+
| 3| 0| 0|
+--+--+--+
| 4| 0| 2|
+--+--+--+
Set 5 to (1, 1)
+--+--+--+
| 0| 1| 0|
+--+--+--+
| 3| 5| 0|
+--+--+--+
| 4| 0| 2|
+--+--+--+
Set 6 to (2, 0)
+--+--+--+
| 0| 1| 6|
+--+--+--+
| 3| 5| 0|
+--+--+--+
| 4| 0| 2|
+--+--+--+
Set 7 to (2, 1)
+--+--+--+
| 0| 1| 6|
+--+--+--+
| 3| 5| 7|
+--+--+--+
| 4| 0| 2|
+--+--+--+
Set 8 to (0, 0)
+--+--+--+
| 8| 1| 6|
+--+--+--+
| 3| 5| 7|
+--+--+--+
| 4| 0| 2|
+--+--+--+
Set 9 to (1, 2)
+--+--+--+
| 8| 1| 6|
+--+--+--+
| 3| 5| 7|
+--+--+--+
| 4| 9| 2|
+--+--+--+

ヒントと注意

displayメソッドですが,

  • System.out.printf を使って桁がそろうように表示してください.サイズ9までは2桁で大丈夫です.
  • まずは,罫線なしで表示するように書いて(2重ループを使います),それが動いたら罫線付きに挑戦しましょう.
  • 罫線付きは,横方向の罫線(上の例だと"+--+--+--+")を表示するメソッドを定義するとシンプルに書けます.
    • 当然,魔方陣の大きさによって罫線の長さは変わります.
  • 余裕がある人版1: 各行,各列,対角線のそれぞれの合計も適当な場所に表示するようにしてください.
  • 余裕がある人版2: 罫線を,JISの罫線素片(┌,┼,└など)を使って表示してみてください.

Copyright (C) 2002-2015 Kota Abe, Osaka City University

ナビゲーション