こんにちは。「Javaを復習する初心者」です。
基本情報技術者試験平成31年度春期のJava問題を見て、そこに出てくるプログラム3が面白いと思ったので、解説を書いてみようと思いました。この記事では、回答部分を正解で埋めたプログラムを載せてますので、正解を知りたくない人は注意してください。
今回は迷路を定義するMazeクラスについて書きます。
試験に登場するクラス
試験にはMazeというクラスが登場します。
以下が試験問題に登場したMazeクラスです。回答部分を埋めてあります。
Mazeクラス
public class Maze { private final String mazeData; private final int width; private final Location startLocation; public Maze(String mazeData, int width) { this.mazeData = mazeData; this.width = width; startLocation = locationOf('S'); } public Location getStartLocation() { return startLocation; } public boolean isGoal(Location loc) { return mazeData.charAt(loc.y * width + loc.x) == 'G'; } public boolean isBlank(Location loc) { return mazeData.charAt(loc.y * width + loc.x) != '*'; } private Location locationOf(char c) { int index = mazeData.indexOf(c); return new Location(index % width, index / width); } }
「出典:平成31年度 春期 基本情報技術者試験 午後 問11」
このクラスはLocationという独自のクラスを使っています。問題文に定義が載っています。
以下のように定義されています。
Locationクラス
public class Location { public final int x, y; public Location(int x, int y) { this.x = x; this.y = y; } }
「出典:平成31年度 春期 基本情報技術者試験 午後 問11」
このクラスはメンバ変数x, yを持っているだけです。この2つの変数はコンストラクタで初期化されます。
final修飾子が付与されているため、コンストラクタで代入する以外では値を変更することは出来ません。例えば、Testクラスを使って、そのmainメソッドで次のよう変数xの値を変更する式を記述するとEclipseではコンパイルエラーの赤線が表示されます。
ソース
Location l = new Location(10, 11);
l.x = 10;
エラーの内容は以下です。
- final フィールド Location.x には代入できません
このようにメンバ変数に代入出来ない作りになっています。
上記のようにfinal修飾子を使ったクラスはあまり見たことはありません。私がよく見るのはprivate修飾子とゲッターメソッドだけを定義したものです。以下のような定義でも外部からメンバ変数の値は変更できないようになってます。
例
public class Location {
private int x, y;
public Location(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
試験ではfinalを使った方の作り方が良く登場するのかもしれません。紙面の都合もある気もします。
番号を(X, Y)座標に変換
Mazeクラスコンストラクタの引数について、問題文で説明がありますが、ポイントは以下です。
- 第1引数mazeDataは文字列で、一文字が迷路の一文字を表す。
- 「S」はスタートのマス、「G」はゴールのマス
- 「*」は壁
- それ以外は通路
- 第2引数widthは右端から左端までのマスの個数を表す。
widthで横方向のマスの数を決めているということですが、これは0,1,2,..という列を2次元のマスに対応させる方法を知っていれば、自然な事と思います。
有名な方法はwidthの値が決まっているときに、次のように0から始まる数iをX座標とY座標を対応させることでしょう。
- x = i % width
- y = i / width
例えば、次のプログラムを動かしてみると実感がわくと思います。
サンプル
int width = 7;
int x, y;
for(int i = 0; i < width * 6; i++) {
x = i % width;
y = i / width;
System.out.printf("(%d, %d)", x, y);
if (x == width - 1) {
// 改行する
System.out.println();
}
}
結果
(0, 0)(1, 0)(2, 0)(3, 0)(4, 0)(5, 0)(6, 0)
(0, 1)(1, 1)(2, 1)(3, 1)(4, 1)(5, 1)(6, 1)
(0, 2)(1, 2)(2, 2)(3, 2)(4, 2)(5, 2)(6, 2)
(0, 3)(1, 3)(2, 3)(3, 3)(4, 3)(5, 3)(6, 3)
(0, 4)(1, 4)(2, 4)(3, 4)(4, 4)(5, 4)(6, 4)
(0, 5)(1, 5)(2, 5)(3, 5)(4, 5)(5, 5)(6, 5)
上記の出力結果を升目のように見ることで、座標(X, Y)が一マスに対応していることがわかると思います。ただし、番号0が(0, 0)です。
上記の対応のXとYから元の番号を計算する式は以下です。
- y * width + x
Mazeクラスに定義されたisGoal()メソッドとisBlank()メソッドでは上記で計算した値をString#charAt()メソッドの引数に指定しています。このメソッドは文字列の先頭を0番目として数えたときのi番目の文字を返却します。それを知っていれば、前述の2つのメソッドはそれぞれ、座標(X, Y)の文字がゴールなのか、壁なのかの真偽を返却していることが分かります。
String#charAt()メソッドで得られる文字の様子は以下のようなサンプルを動かしてみると良いと思います。
ソース
int width = 7;
for (int y = 0; y < s.length() / width; y++) {
for(int x = 0; x < width; x++) {
int index = y * width + x;
System.out.print(s.charAt(index));
}
// 改行する
System.out.println();
}
結果
ABCDEFG
HIJKLMN
abcdefg
hijklmn
順番通りに出力されています。