クラス、スーパークラス、抽象クラス、staticフィールド、インスタンスフィールド、staticメソッド、インスタンスメソッドなどを演習課題を通じて学習する。
文法の詳細については、
などを参考にすること。
ファイル AnimalGroup.java を作り、以下のプログラムを作りなさい。
まず、一般的な動物を表す抽象クラス Animal を以下のように作る。
| 種別 | 型 名前 など | 説明 | 設定・内容 | インスタンスフィールド | private final String name | その動物インスタンスの名前を表す文字列 | (初期値設定なし = null。コンストラクタで値設定された後は変更不可) | コンストラクタ | Animal(String name) | 動物インスタンス生成時に呼ばれる | nameフィールドにnameパラメータの値をセットする | インスタンスメソッド | String getType() | その動物インスタンスに対応する種類を得る | (抽象メソッド) | インスタンスメソッド | final String getName() | その動物インスタンスに対応する名前を得る | name を返す (オーバーライド不可) |
インスタンスメソッド | final String sender() | その動物インスタンスのプロンプトを得る | name + "> " を返す (オーバーライド不可) |
インスタンスメソッド | void makeSounds() | その動物インスタンスの鳴き声を表示する | sender() + "..." を表示し改行する |
|---|
実際のコードは下にあるが、可能な限り自分で作ってみてほしい。
abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
abstract String getType();
final String getName() {
return name;
}
final String sender() {
return name + "> ";
}
void makeSounds() {
System.out.println(sender() + "...");
}
}
getName メソッドと sender メソッドは final が指定されているので、サブクラスでオーバーライド(上書き)することができない。
上の Animal クラスをスーパークラスとして、
を作る。 例として、Dog クラスは以下のようになる。スーパークラスである Animal のコンストラクタが name パラメータを必要とするので、Dog コンストラクタの中では最初に super(name); とする必要があることに注意する。
class Dog extends Animal { private static final String type = "dog"; Dog(String name) { super(name); } @Override String getType() { return type; } @Override void makeSounds() { System.out.println(sender() + "Bow-wow"); } }
上の Dog クラスを参考に、Cat クラス、Lion クラス、Shellfish クラスを書きなさい。 Dog クラスをコピーして貼り、以下の変更を加える必要がある。
ただし、貝を表す Shellfish クラスについては、貝は鳴かないので makeSounds メソッドをオーバーライドしないこと。
次に、ソースファイルの先頭に AnimalGroup クラスを作り、中に mainメソッドを入れる。その際、下記のjavadocコメントも入れ、学籍番号・氏名は自分のものとする。
/**
* 演習12 : 動物クラスと継承クラス.
* @author 学籍番号・氏名
*/
public class AnimalGroup {
public static void main(String[] args) {
}
}
mainメソッドの中で、Animalの配列 animals を定義し、その中に以下を入れる。
| 動物 | 名前 |
|---|---|
| ライオン | Simba |
| 猫 | Felix |
| 犬 | Muttley |
| ライオン | Leo |
| 貝 | Clam |
拡張for構文で animals の中の各動物に対し、その名前と動物の種類を
の形式でスペース区切りで表示し、最後に改行させる。
拡張for構文で animals の中の各動物に対し、makeSounds メソッドを呼び出す。
以下をテキストファイル (拡張子 txt) としてまとめる。
解答例 (演習課題名、クラス名が異なる可能性あり)
じゃんけんゲームを多数回行うプログラムを通じて、Javaにおける抽象クラス・継承・ポリモフィズムを使ったオブジェクト指向プログラミングを学習する。
オブジェクト指向による開発は、現在実際の現場で多く使用されているが、10~30行程度の短い練習用プログラムでは、ありがた味が分からない。そのため、今回の課題プログラムは350行以上あり、初めて長いプログラムに取り組む人は少々大変かも知れないが、大部分は既に出来ており追加部分は少ししかないので、真剣に取り組めば十分解決できる。後でもいいので、ソースの全体を見直し意味を理解することを強く勧める。それにより、オブジェクト指向プログラミングのエッセンスが習得できる。
以下は、じゃんけんゲームを多数回行い結果の集計を表示するアプリケーションのソースファイル (Janken1.java) である。
抜けている部分を補って動作させなさい。
プレーヤを表すクラス Player は抽象クラスで、実際のプレーヤクラスは Player を継承し、すべての抽象メソッドをオーバーライドする具象クラスとなる。
上記コードでは、具象クラスとして、
を用意してある。
ゲームを表すクラス Game のオブジェクトは、コンストラクタで各プレーヤを引数で指定し生成する。
クラス Game のインスタンス変数配列
private final Player[] players;
にゲームの全プレーヤが格納されている。
メインクラス Janken1 の main メソッドでは、
まず各プレーヤを指定して Game オブジェクトを生成する。
次に、Game オブジェクトの match メソッドを呼び出し、多数回の対戦と集計を行う。
match メソッドの中では、for文で指定されたゲーム数の繰り返しを行い、各回で、
play(moves, results);
のように、勝負を1回する play メソッドを呼び出す。各プレーヤの選択手を入れる配列 moves および 各プレーヤの結果を入れる配列 results がパラメータとして渡される。
playメソッドの中では、拡張for構文による
for (Player p : players) {
moves[p.number()] = p.next();
}
により、Playerクラスの next メソッドで各プレーヤ p の選択が決定され、moves 配列に入る。
next メソッドでは、実際に選択を行うメソッド select を呼ぶが、ポリモフィズムにより、オブジェクトの実際の型に応じた select が呼ばれる。
その後、setResult メソッドにより、全プレーヤの選択手を基に全プレーヤの勝負の結果を設定する。
playメソッドから抜け、matchメソッドに戻った後、拡張for構文の中で、各プレーヤ p は
int n = p.number(); p.result(results[n], moves.clone());
のように result メソッドで全プレーヤの選択・勝敗の結果を受け取る。 各プレーヤはこれらの結果を次回以降の選択の参考にすることができる。 result メソッドもポリモフィズムによりオブジェクトの実際の型に応じたものが呼ばれる。
以下では、このプログラムで利用しているJavaの記述についての補足的な説明を行う。
ゲームの結果は列挙型(クラス) Result で表す。enumの書き方については、
を参考にすること。
Game クラスの最初の辺りに
static { for (int i = 0; i < MOVES; i++) { ・・・・・・ } }
という部分があるが、これは staticイニシャライザ でクラスのロード時に一度だけ実行される。static変数の初期化に使用されるもので、インスタンス変数にはアクセスできない。
その50行程度下にGameクラスのコンストラクタの定義があるが、
Game(Player... p) {
・・・・・・
}
のように、パラメータ指定に 可変長引数 (Java 5 で導入された) を使っている。「Player... p」は、Player型のパラメータが任意個あるという意味で、個々のパラメータには配列のようにしてアクセスできる。このケースでは p[0] が最初のパラメータ、p[1] が二番目のパラメータ、... を表し、パラメータの個数は p.length で求められる。
ソース (TODO部分、抜けあり)
import java.util.Random; /** * 演習13 : メインクラス、じゃんけんプレーヤクラス. * * じゃんけんゲームを多数回行い、結果の集計を表示するアプリケーションのメインクラス * @author ここに自分の学籍番号・氏名を記入 */ public class Janken1 { public static void main(String[] args) { // TODO // 以下でプレーヤを指定してゲームオブジェクトを生成 Game game = new Game(プレーヤ1 (パラメータ 0 を指定する) を生成, プレーヤ2 (パラメータ 1 を指定する) を生成); // 多数回対戦 // TODO // 必要に応じて、ゲーム数および詳細出力のパラメータを変える game.match(100, true); } } /** * ゲーム結果を表すenumクラス */ // TODO // 下に、定数 WIN, DRAW, LOSS を持つ 列挙型 (クラス) Result を書く。 /** * プレーヤのベースクラス. * 抽象クラス */ abstract class Player { private final int number_; // プレーヤ番号(順番, 0始まり) /** * コンストラクタ * @param n プレーヤ番号(順番, 0始まり) */ Player(int n) { number_ = n; } /** * プレーヤ番号(順番, 0始まり)を返すメソッド * @return プレーヤ番号(順番, 0始まり) */ final int number() { return number_; } /** * プレーヤ種類を返すメソッド * @return プレーヤ種類 */ // TODO // 下に String を返す type メソッド (抽象メソッドとする) の // 宣言を書く。 /** * 結果を受け取るメソッド * @param r プレーヤに対する結果 * @param moves 全プレーヤの選択を持つ配列 */ // TODO // 下に void 型の result メソッド (パラメータとして Result r, int[] moves を // 持つ抽象メソッドとする) の宣言を書く。 /** * エラーチェックつきで次の選択を送るメソッド * @return 次の選択手 * @throws IndexOutOfBoundsException selectメソッドから返された選択が範囲外のとき */ final int next() { // TODO // 下で、select メソッドを呼び出し、戻り値を int 型変数 m に代入する if (m < 0 || m >= Game.MOVES) throw new IndexOutOfBoundsException("選択 " + m + " が範囲外"); return m; } /** * 次の選択を決定するメソッド * @return 次の選択手 */ // TODO // 下に int を返す select メソッド (抽象メソッドとする) の // 宣言を書く。 } /** * ランダムプレーヤクラス */ final class RandomPlayer extends Player { private Random rand = new Random(); /** * コンストラクタ * @param n プレーヤ番号(順番, 0始まり) */ RandomPlayer(int n) { super(n); } @Override String type() { return "ランダム"; } /** * 結果を受け取るメソッド. * このプレーヤでは、何もせず無視する * @param r プレーヤに対する結果 * @param moves 全プレーヤの選択を持つ配列 */ @Override void result(Result r, int[] moves) {} @Override int select() { return rand.nextInt(Game.MOVES); } } /** * 直前の相手の選択手に勝つ手を選ぶプレーヤクラス. * 3人以上の対戦のとき、3種出てあいこになったときには自分の直前手に勝つ手を選ぶ */ final class LatestWinPlayer extends Player { private int prevMove; // 直前の自分の選択 private int oppMove; // 直前の相手の選択 /** * コンストラクタ * @param n プレーヤ番号(順番, 0始まり) */ LatestWinPlayer(int n) { super(n); Random rand = new Random(); oppMove = rand.nextInt(Game.MOVES); } @Override String type() { return "直前勝ち手"; } /** * 結果を受け取るメソッド. * このプレーヤでは、結果に応じて oppMove をセットする * @param r プレーヤに対する結果 * @param moves 全プレーヤの選択を持つ配列 */ @Override void result(Result r, int[] moves) { switch (r) { case WIN: // TODO // 下で、Gameクラスのstaticメソッドを呼び、prevMoveに負ける手を // 変数 oppMove に代入する。 break; case DRAW: oppMove = prevMove; break; case LOSS: // TODO // 下で、Gameクラスのstaticメソッドを呼び、prevMoveに勝つ手を // 変数 oppMove に代入する。 break; } } /** * 次の選択を決定するメソッド */ @Override int select() { prevMove = Game.winMove(oppMove); // 直前の相手の選択手に勝つ手 return prevMove; } } /** * ゲームを表現するクラス */ final class Game { /** * 選択数 */ static final int MOVES = 3; /** * グー */ static final int GUU = 0; /** * チョキ */ static final int CHOKI = 1; /** * パー */ static final int PAA = 2; /** * 各選択肢を表す文字列配列 */ private static final String[] moveStr = {"G", "C", "P"}; /** * プレーヤから見た結果表 [自分の選択][自分以外の選択] */ private static final Result[][] resultMat = { {Result.DRAW, Result.WIN, Result.LOSS}, {Result.LOSS, Result.DRAW, Result.WIN}, {Result.WIN, Result.LOSS, Result.DRAW} }; private static final int[] winMoves = new int[MOVES]; // 選択手に勝つ手 private static final int[] lossMoves = new int[MOVES]; // 選択手に負ける手 static { for (int i = 0; i < MOVES; i++) { // 選択手に勝つ手を求める for (int j = 0; j < MOVES; j++) { if (resultMat[j][i] == Result.WIN) { winMoves[i] = j; break; } } // 選択手に負ける手を求める for (int j = 0; j < MOVES; j++) { if (resultMat[j][i] == Result.LOSS) { lossMoves[i] = j; break; } } } } /** * 指定された手に勝つ手を求める * @param m 選択手 * @return 指定された選択手に勝つ手 */ static int winMove(int m) { return winMoves[m]; } /** * 指定された手に負ける手を求める * @param m 選択手 * @return 指定された選択手に負ける手 */ static int lossMove(int m) { return lossMoves[m]; } /** * プレーヤ数 */ private final int numPlayers; /** * プレーヤ配列 */ private final Player[] players; /** * コンストラクタ * @param p 各プレーヤを表すオブジェクトの可変長引数 * @throws IllegalArgumentException プレーヤ数またはプレーヤ番号が不正のとき */ Game(Player... p) { if (p.length <= 1) throw new IllegalArgumentException("プレーヤ数 " + p.length + " が不正"); numPlayers = p.length; players = new Player[numPlayers]; for (int i = 0; i < numPlayers; i++) { if (p[i].number() != i) throw new IllegalArgumentException("プレーヤ " + i + " の番号 " + p[i].number() + " が不正"); players[i] = p[i]; } } /** * 全プレーヤの選択に基づき、全プレーヤに対する結果を求める. * 一般n人用 */ private void setResult(Result[] results, int[] moves) { int[] count = new int[MOVES]; int numTypes = 0; for (int m : moves) { if (count[m] == 0) numTypes++; count[m]++; } if (numTypes == 1 || numTypes == 3) { for (int i = 0; i < numPlayers; i++) { results[i] = Result.DRAW; } return; } assert numTypes == 2; int m1 = moves[0]; int m2 = -1; for (int i = 1; i < numPlayers; i++) { if (moves[i] != m1) { m2 = moves[i]; break; } } Result r1 = resultMat[m1][m2]; Result r2 = resultMat[m2][m1]; for (int i = 0; i < numPlayers; i++) { if (moves[i] == m1) results[i] = r1; else // if (moves[i] == m2) results[i] = r2; } } /** * ゲームを1回勝負する * @param moves 各プレーヤの選択手を入れる配列 * @param results 各プレーヤの結果を入れる配列 */ void play(int[] moves, Result[] results) { for (Player p : players) { moves[p.number()] = p.next(); } setResult(results, moves); } /** * ゲームを多数回行い結果の集計を表示する * @param numGames ゲーム数 * @param showDetail ゲーム毎の結果を表示するかどうか */ void match(int numGames, boolean showDetail) { System.out.print(numPlayers + "人ゲーム; プレーヤ"); for (Player p : players) { System.out.print(" " + (p.number() + 1) + ":" + p.type()); } System.out.println(); int[] winCount = new int[numPlayers]; int drawCount = 0; for (int g = 0; g < numGames; g++) { int[] moves = new int[numPlayers]; Result[] results = new Result[numPlayers]; play(moves, results); if (results[0] == Result.DRAW) { drawCount++; } for (Player p : players) { int n = p.number(); p.result(results[n], moves.clone()); if (results[n] == Result.WIN) { winCount[n]++; } } if (showDetail) { for (Player p : players) { System.out.print(" " + (p.number() + 1) + ":" + moveStr[moves[p.number()]]); } int wc = 0; for (int n = 0; n < numPlayers; n++) { if (results[n] == Result.WIN) { wc++; System.out.print(" " + (n + 1) + "win"); } } if (wc == 0) { System.out.println(" " + "draw"); } else { System.out.println(); } } } System.out.print(numGames + " games:"); int wins = 0; for (Player p : players) { int n = p.number(); System.out.print(" " + (n + 1) + "win=" + winCount[n]); wins += winCount[n]; } System.out.println(" draw=" + drawCount); } }
以下の組合せでそれぞれ数回ずつ対戦させてみなさい。
動作が確かめられたら、多数回対戦のゲーム数を 100000、詳細出力を false にして、上記 1. ~ 3. の出力の各一例を求めなさい。
Playerクラスを継承し、特定の動作をするプレーヤのクラスを作るためには、以下のメソッドを作成する必要がある。
| メソッド | 内容 |
|---|---|
| コンストラクタ | インスタンス生成時に呼ばれる。 最初にスーパークラスのコンストラクタをパラメータ付きで呼び出す必要がある。 |
| type | プレーヤ種類を表す文字列を返す |
| result | 直前の自分の勝敗と全プレーヤの選択を受け取る |
| select | 次の選択を決定して返す |
以下の課題では、それらのメソッドをオーバーライドして新しいプレーヤを作り、各種のプレーヤと連続対戦させて動作を調べる。
上記演習のファイルをコピーし Janken2.java という名前に変更する。
メインクラスも Janken2 に名前変更し、ソースの中のクラス名使用部分も全部変更する。
また、javadocの「演習番号 : 演習名」のところは「今回の課題番号 : 課題名」に変更する。
クラス LatestWinPlayer を参考に、クラス Player を継承した
直前の相手の選択手に負ける手を選ぶプレーヤクラス LatestLossPlayer
を追加しなさい。type メソッドで返されるのは "直前負け手" とする。
また、クラス Player を継承し、
グー・チョキ・パーを決まった順番 (6通りのうちの一つ。生成時にどれか一つランダムに決定する) で出すのを繰り返すプレーヤクラス RepeatPlayer
を追加しなさい。type メソッドで返されるのは "順番繰り返し" とする。
クラス LatestWinPlayer でも、クラス RepeatPlayer でも、スーパークラスのパラメータを持つコンストラクタは自動的には呼び出されないので、int n をパラメータとして持つコンストラクタを記述し、その最初に
super(n);
として、スーパークラスである Player のコンストラクタを明示的に呼び出すこと。
以下の組合せで数回対戦させてみなさい。
動作が確かめられたら、多数回対戦のゲーム数を 13、詳細出力を true にして、上記 1. ~ 5. の出力の一例を求めなさい。
をテキストファイルにまとめ、講義のWebページから提出しなさい。
前課題の クラス Player、クラス RandomPlayer、クラス LatestWinPlayer、クラス LatestLossPlayer、クラス RepeatPlayer、クラス Game を UML の詳細クラス図として示しなさい。
※ UMLについて
今回の演習では、おおよそ、privateな変数・メソッド、static変数・メソッドは無視してよいが、クラス Game の players はクラスの関連付けに活かし、numPlayers はクラスの属性として表現しなさい。
抽象クラスの名前、抽象メソッドはUMLでは斜体フォントで表現する。
クラスの要点部分はソースを見て自分でまとめた方が力がつく。どうしても無理な人は
を参考にすること。
※ クラス図は、例えば Microsoft Office 形式の文書ファイルで、図形編集機能を使って作成できる。
解答例 (演習番号が異なる可能性あり)
上記課題のファイルをコピーし Janken3.java という名前に変更する。
メインクラスも Janken3 に名前変更し、ソースの中のクラス名使用部分も全部変更する。
また、javadocの「課題番号 : 課題名」も変更する。
クラス Player を継承して、
自前のプレーヤクラス Player学籍番号
を追加しなさい。「学籍番号」部分は各自の学籍番号とする。type メソッドで返されるのは "各自の学籍番号" とする。
クラス Player学籍番号 部分以外のソースは変更しないものとする。
以下の組合せでそれぞれ数回ずつ対戦させてみなさい。
なお、上記 1. ~ 4. はあくまでも例であり、クラス Player学籍番号 は「vs. RandomPlayer」を除く任意の対戦組合せにおいて同等の成績を上げられなければならない。
動作が確かめられたら、多数回対戦のゲーム数を 100000、詳細出力を false にして、上記 1. ~ 4. の出力の各一例を求めなさい。
をテキストファイルにまとめ、講義のWebページから提出しなさい。
条件として、「vs. RandomPlayer」を除き、Player学籍番号 の勝率が 95% 以上でなければならない。
条件を満足できない場合は課題提出をしないこと。
上記課題のファイルをコピーし Janken4.java という名前に変更する。
メインクラスも Janken4 に名前変更し、ソースの中のクラス名使用部分も全部変更する。
また、javadocの「課題番号 : 課題名」も変更する。
自前プレーヤクラス「Player学籍番号」を3人ゲームに対応するよう変更しなさい。
クラス Player学籍番号 部分以外のソースは変更しないものとする。
mainメソッドのGameオブジェクト生成部分を変更し、以下の組合せでそれぞれ数回ずつ連続対戦させてみなさい。
なお、上記 1. ~ 3. はあくまでも例であり、クラス Player学籍番号 は「vs. RandomPlayer 2つ」を除く任意の対戦組合せにおいて同等以上の成績を上げられなければならない。
動作が確かめられたら、多数回対戦のゲーム数を 100000、詳細出力を false にして、上記 1. ~ 3. の出力の各一例を求めなさい。
をテキストファイルにまとめ、講義のWebページから提出しなさい。
条件として、 1. ~ 3. の出力が全部、Player学籍番号 の勝率が 65% 以上でなければならない。
条件を満足できない場合は課題提出をしないこと。
インタフェース (interface) は、static定数と抽象メソッドのみを持つ究極の抽象クラスと考えられる。ただ、新しいJavaでは、default キーワードをつけてメソッドのデフォルト実装を記述することができるようになった。
インタフェースの中のstatic定数およびメソッドはすべて public となる。
文法の詳細については、
を参考にすること。
抽象クラスを使っていたプログラムをインタフェースを使って書き換える演習を行なう。あくまで演習課題として用意したものなので、インタフェースにすることの是非は不問とする。
演習12のファイル AnimalGroup.java をコピーして AnimalGroupI.java というファイル名に変更し、抽象クラス Animal でなく、インタフェース Animal として利用するプログラムに作り変えなさい。
以下ではやり方を解説しているが、本当にプログラミングの力をつけたいと考えている人は、まず自分で考えてやってみること。どうしてもできない時に下を参考にしてほしい。
まず、Animal をインタフェースとして以下のように作り変える。
| 種別 | 型 名前 など | 説明 | 設定・内容 | メソッド | String getType() | その動物インスタンスに対応する種類を得る | (抽象メソッド) | メソッド | String sender() | その動物インスタンスのプロンプトを得る | (デフォルト実装) getName() + "> " を返す | メソッド | String sender() | その動物インスタンスのプロンプトを得る | (抽象メソッド) | メソッド | void makeSounds() | その動物インスタンスの鳴き声を表示する | (デフォルト実装) sender() + "..." を表示し改行する |
|---|
実際のコードは下記となる。インタフェースのメソッドは通常は抽象メソッドなので本体は記述せずセミコロンで終わる。ただし、デフォルト実装 がある場合はキーワード default をつけてメソッドを処理本体ごと記述する。
interface Animal {
String getType();
String getName();
default String sender() {
return getName() + "> ";
}
default void makeSounds() {
System.out.println(sender() + "...");
}
}
上ではメソッドに abstract も public もつけていないが、インタフェースの中のdefault実装のないメソッドはすべて abstract なので明示的に abstract をつけて記述してもよい。また、インタフェースの中のメソッドはすべて public なので、各メソッドに public を明示的につけて記述してもよい。public のみをつけると下記のようになる。
public String getType();
public String getName();
default public String sender() {
return getName() + "> ";
}
default public void makeSounds() {
System.out.println(sender() + "...");
}
上の Animal インタフェースを実装するクラスとして、
を作る。例として、Dog クラスは以下のようになる。演習12では name フィールドはスーパークラスにあったが、本課題では Dog クラス自体が name フィールドを持っていることに注意する。
インタフェースの場合は継承や拡張といわず 実装 (implementation) という。キーワードも extends でなく implements を使ってインタフェースを指定する。
また、インタフェースのメソッドはすべて public となるので、オーバーライド時には public をつけて指定する必要がある。
class Dog implements Animal { private static final String type = "dog"; private final String name; Dog(String name) { this.name = name; } @Override public String getName() { return name; } @Override public String getType() { return type; } @Override public void makeSounds() { System.out.println(sender() + "Bow-wow"); } }
上の Dog クラスを参考に、Cat クラス、Lion クラス、Shellfish クラスを書きなさい。 Dog クラスをコピーして貼り、以下の変更を加える必要がある。
ただし、貝を表す Shellfish クラスについては、貝は鳴かないので makeSounds メソッドをオーバーライドしないこと。
次に、ソースファイル先頭の AnimalGroup クラスを AnimalGroupI と名前変更し、javadocコメントの「演習番号 : 演習名」を修正する。
/** * 演習15 : 動物インタフェースと各種実装クラス. * @author 学籍番号・氏名 */ public class AnimalGroupI {
mainメソッドの中味は修正する必要がない。 ただし、演習12と意味は異なる。演習12では、Animal の配列である Animal[] animals の各要素には、
抽象クラス Animal を継承するクラスのインスタンス
が入っていたが、本演習では、
インタフェース Animal を実装する何らかのクラスのインスタンス
が入っている。このように、インタフェースを変数の型として使用できる。
以下をテキストファイル (拡張子 txt) としてまとめる。
解答例 (演習課題名、クラス名が異なる可能性あり)