以下のようなプログラムについて考えます。
Dog dog = new WiseDog("ポチ"); dog.cry();
このプログラムの 1行目のように、Javaでは、参照型変数の型(Dog)とインスタンスの型(WiseDog)が異なる場合がありました。そして、dog.cry();
のメソッド呼び出しでは、インスタンスの型(WiseDog)の cryメソッドが呼び出されるのでした。
と言うことは、いろいろなクラスを用意した場合には、dog.cry();
の処理内容はさまざまに変わる可能性があります。この性質のことを多態性と言います。本節では、2つのサンプルプログラムで、多態性を見ていきます。
なお、多態性とはポリモルフィズムとも呼ばれます。
これまで製作してきた Dog系クラス、Cat系クラス、Crow系クラスをすべてまとめて、多態性の動きを確認します。
イメージとしては以下のような感じです。このイメージの中に本来はメソッドも記述すべきですが、省略しています。
Animalクラス、Dogクラス、WiseDogクラス、Catクラス、WiseCatクラスは M502プロジェクトのものをそのまま利用、Birdクラス、Crowクラスh M503プロジェクトのものをそのまま利用、HowlingDogクラスは M204プロジェクトのものをそのまま利用しています。
ソースコードは以下の通り。
M601/Animal.java
/** * 動物を表す抽象クラスです。 */ public abstract class Animal { /** * 名前。 */ protected String name; /** * 鳴く回数。 */ protected int cryCount; /** * 名前を設定します。 * @param name 名前 */ public void setName(String name) { this.name = name; } /** * 鳴く回数を設定します。 * @param cryCount 鳴く回数 */ public void setCryCount(int cryCount) { this.cryCount = cryCount; } /** * 鳴きます。 */ public abstract void cry(); }
M601/Dog.java
/** * 犬を表すクラスです。 */ public class Dog extends Animal { /** * コンストラクタ。 * @param name 名前 */ public Dog(String name) { this(name, 1); } /** * コンストラクタ。 * @param name 名前 * @param cryCount 鳴く回数 */ public Dog(String name, int cryCount) { this.setName(name); this.setCryCount(cryCount); } @Override public void cry() { System.out.print(this.name + "「"); for(int i = 0; i < this.cryCount; i ++) { System.out.print("ワン"); } System.out.println("」"); } /** * お座りします。 */ public void sitDown() { System.out.println(this.name + "は座りました。"); } @Override public String toString() { return "[Dog] name:" + this.name + ", cryCount:" + this.cryCount; } }
M601/WiseDog.java
/** * 賢い犬を表すクラスです。 */ public class WiseDog extends Dog { /** * コンストラクタ。 * @param name 名前 */ public WiseDog(String name) { super(name); } /** * コンストラクタ。 * @param name 名前 * @param cryCount 鳴く回数 */ public WiseDog(String name, int cryCount) { super(name, cryCount); } @Override public void cry() { System.out.print(this.name + "「"); for(int i = 0; i < this.cryCount; i ++) { System.out.print("キャン"); } System.out.println("」"); } /** * お手をします。 */ public void touch() { System.out.println(this.name + "はお手をしました。"); } @Override public String toString() { return "[WiseDog] name:" + this.name + ", cryCount:" + this.cryCount; } }
M601/HowlingDog.java
/** * 遠吠えする犬を表すクラスです。 */ public class HowlingDog extends Dog { /** * コンストラクタ。 * @param name 名前 */ public HowlingDog(String name) { super(name); } /** * コンストラクタ。 * @param name 名前 * @param cryCount 鳴く回数 */ public HowlingDog(String name, int cryCount) { super(name, cryCount); } @Override public void cry() { int random = (int)(Math.random() * 2); if(random == 0) { // 既存の鳴き声で鳴く super.cry(); } else { // 遠吠えする System.out.print(this.name + "「ワオ"); for(int i = 0; i < this.cryCount; i ++) { System.out.print("ー"); } System.out.println("ン」"); } } }
M601/Cat.java
/** * 猫を表すクラスです。 */ public class Cat extends Animal { /** * コンストラクタ。 */ public Cat() { this("名無しの猫", 1); } /** * コンストラクタ。 * @param name 名前 */ public Cat(String name) { this(name, 1); } /** * コンストラクタ。 * @param name 名前 * @param cryCount 鳴く回数 */ public Cat(String name, int cryCount) { this.setName(name); this.setCryCount(cryCount); } @Override public void cry() { System.out.print(this.name + "「"); for(int i = 0; i < this.cryCount; i ++) { System.out.print("ニャー"); } System.out.println("」"); } @Override public String toString() { return "[Cat] name:" + this.name + ", cryCount:" + this.cryCount; } }
M601/WiseCat.java
/** * 賢い猫を表すクラスです。 */ public class WiseCat extends Cat { /** * コンストラクタ。 */ public WiseCat() { ; // super(); が省略されている } /** * コンストラクタ。 * @param name 名前 */ public WiseCat(String name) { super(name); } /** * コンストラクタ。 * @param name 名前 * @param cryCount 鳴く回数 */ public WiseCat(String name, int cryCount) { super(name, cryCount); } /** * 横になります。 */ public void lie() { System.out.println(this.name + "は横になりました。"); } @Override public String toString() { return "[WiseCat] name:" + this.name + ", cryCount:" + this.cryCount; } }
M601/Bird.java
/** * 鳥を表す抽象クラスです。 */ public abstract class Bird extends Animal { /** * 飛びます。 */ public void fly() { System.out.println(this.name + "は飛びました。"); } }
M601/Crow.java
/** * 犬を表すクラスです。 */ public class Crow extends Bird { /** * コンストラクタ。 */ public Crow() { this(1); } /** * コンストラクタ。 * @param cryCount 鳴く回数 */ public Crow(int cryCount) { this.setName("名無しのカラス"); this.setCryCount(cryCount); } @Override public void cry() { System.out.print(this.name + "「"); for(int i = 0; i < this.cryCount; i ++) { System.out.print("カー"); } System.out.println("」"); } @Override public String toString() { return "[Crow] name:" + this.name + ", cryCount:" + this.cryCount; } }
M601/M601.java
/** * 多態性を確認します。 */ public class M601 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Animal[] animals = new Animal[] { new Dog("ポチ"), new WiseDog("ラッキー", 5), new HowlingDog("ハチ", 10), new Cat(), new WiseCat("タマ", 3), new Crow(5), }; for(int i = 0; i < animals.length; i ++) { animals[i].cry(); } } }
実行結果の例は以下の通り。
ポチ「ワン」 ラッキー「キャンキャンキャンキャンキャン」 ハチ「ワオーーーーーーーーーーン」 名無しの猫「ニャー」 タマ「ニャーニャーニャー」 名無しのカラス「カーカーカーカーカー」
cryメソッドの呼び出しによって、それぞれのインスタンスに応じた処理が行われていますね。これが多態性ということになります。
ただ、このプログラムでは、プログラムの規模が大きく手間がかかった割には全然、利用価値が無いように思えます。そこで、続いて別の例を挙げてみたいと思います。
抽象クラス Questionとそれを継承したクラスによって、多態性を生かしたクイズゲームを作成します。具体的には以下の 4種類の問題形式を入り混ぜたクイズゲームを製作します。
イメージとしては以下のような感じです。このイメージの中に本来はメソッドも記述すべきですが、省略しています。
ソースコードは以下の通り。
M602/MySystem.java
(ライブラリをそのまま利用します)
M602/Question.java
/** * 問題を表す抽象クラスです。 */ public abstract class Question { /** * 問題文を返します。 * @return 問題文 */ public abstract String getQuestion(); /** * 入力が正常かどうかチェックします。 * @param input 入力 * @return 入力が正常の場合 true */ public abstract boolean check(String input); /** * 正解かどうかを返します。 * @param input 入力 * @return 正解の場合 true */ public abstract boolean isCorrect(String input); /** * 解答文を返します。 * @return 解答文 */ public abstract String getAnswer(); }
M602/SimpleQuestion.java
/** * シンプルな問題を表すクラスです。 */ public class SimpleQuestion extends Question { /** * 問題。 */ private String question; /** * 解答。 */ private String answer; /** * コンストラクタ。 * @param question 問題 * @param answer 解答 */ public SimpleQuestion(String question, String answer) { this.question = question; this.answer = answer; } @Override public String getQuestion() { return this.question; } @Override public boolean check(String input) { return true; } @Override public boolean isCorrect(String input) { return this.answer.equals(input); } @Override public String getAnswer() { return this.answer; } }
M602/LengthCheckQuestion.java
/** * 入力文字の長さをチェックする問題を表すクラスです。 */ public class LengthCheckQuestion extends SimpleQuestion { /** * コンストラクタ。 * @param question 問題 * @param answer 解答 */ public LengthCheckQuestion(String question, String answer) { super(question, answer); } @Override public String getQuestion() { return super.getQuestion() + "(正解は " + this.getAnswer().length() + "文字)"; } @Override public boolean check(String input) { return this.getAnswer().length() == input.length(); } }
M602/YesNoQuestion.java
/** * 「はい」または「いいえ」を問う問題を表すクラスです。 */ public class YesNoQuestion extends Question { /** * 問題。 */ private String question; /** * 解答。 */ private boolean answer; /** * コンストラクタ。 * @param question 問題 * @param answer 解答 */ public YesNoQuestion(String question, boolean answer) { this.question = question; this.answer = answer; } @Override public String getQuestion() { return this.question + "(y/n)"; } @Override public boolean check(String input) { if(input.equals("y") || input.equals("Y")) { return true; } else if(input.equals("n") || input.equals("N")) { return true; } else { return false; } } @Override public boolean isCorrect(String input) { if(input.equals("y") || input.equals("Y")) { // 「はい」と答えた場合は、正しい場合に trueを返す return this.answer; } else if(input.equals("n") || input.equals("N")) { // 「いいえ」と答えた場合は、正しくない場合に trueを返す return ! this.answer; } else { // プログラムの流れが正しければ、決してここには到達しない // 例外については、「エラーと例外」の章を参照すること throw new IllegalArgumentException(); } } @Override public String getAnswer() { return this.answer ? "yes" : "no"; } }
M602/SelectedQuestion.java
/** * 選択する問題を表します。 */ public class SelectedQuestion extends Question { /** * 問題。 */ private String question; /** * 解答群。 */ private String[] answers; /** * 解答。 */ private String answer; /** * コンストラクタ。 * @param question 問題 * @param answers 解答群(最初の解答を正解に設定すること) */ public SelectedQuestion(String question, String[] answers) { this.question = question; this.answer = answers[0]; // 解答群をコピーする this.answers = new String[answers.length]; for(int i = 0; i < answers.length; i ++) { this.answers[i] = answers[i]; } // 解答群をシャッフルする this.shuffleAnswers(); } /** * 解答群をシャッフルします。 */ private void shuffleAnswers() { for(int i = 0; i < this.answers.length; i ++) { int value = (int)(Math.random() * this.answers.length); String temp = this.answers[value]; this.answers[value] = this.answers[i]; this.answers[i] = temp; } } @Override public String getQuestion() { StringBuilder sb = new StringBuilder(); sb.append(this.question + "("); for(int i = 0; i < this.answers.length; i ++) { sb.append(this.answers[i]); if(i != this.answers.length - 1) { sb.append(","); } } sb.append(")"); return sb.toString(); } @Override public boolean check(String input) { for(int i = 0; i < this.answers.length; i ++) { if(this.answers[i].equals(input)) { return true; } } return false; } @Override public boolean isCorrect(String input) { return this.answer.equals(input); } @Override public String getAnswer() { return this.answer; } }
M602/M602.java
/** * クイズゲームです。 */ public class M602 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { // 問題群を生成する Question[] questions = new Question[] { new SimpleQuestion("りんごを表す英単語は", "apple"), new SimpleQuestion("みかんを表す英単語は", "orange"), new LengthCheckQuestion("太陽を表す英単語は", "sun"), new LengthCheckQuestion("月を表す英単語は", "moon"), new YesNoQuestion("womanの複数形は women", true), new YesNoQuestion("forgetの過去形は forgetted", false), new SelectedQuestion("火曜日を表す英単語は", new String[] { "Tuesday", "Monday", "Wednesday", "Thursday", "Friday", }), new SelectedQuestion("childの複数形は", new String[] { "children", "child", "childs", "childes", }), }; // 正解数 int score = 0; // 問題群をシャッフルする M602.shuffleQuestions(questions); System.out.println("------------"); System.out.println("クイズゲーム"); System.out.println("------------"); for(int i = 0; i < questions.length; i ++) { Question question = questions[i]; System.out.println("第 " + (i + 1) + "問:"); String input; // ユーザからの入力を受け付ける do { input = MySystem.in.getString(question.getQuestion()); input = input.trim(); } while(! question.check(input)); // 正解かどうか判定をする boolean isCorrect = question.isCorrect(input); if(isCorrect) { System.out.println("正解!"); score ++; } else { System.out.println("不正解。正解は " + question.getAnswer() + "でした。"); } } // ゲームを終了する System.out.println("------------"); System.out.println(questions.length + "問中 " + score + "問正解でした。"); } /** * 引数に設定した問題群をシャッフルします。 * @param questions 問題群 */ private static void shuffleQuestions(Question[] questions) { for(int i = 0; i < questions.length; i ++) { int value = (int)(Math.random() * questions.length); Question temp = questions[value]; questions[value] = questions[i]; questions[i] = temp; } } }
実行結果の例は以下の通り。
------------ クイズゲーム ------------ 第 1問: 太陽を表す英単語は(正解は 3文字)? moon 太陽を表す英単語は(正解は 3文字)? sun 正解! 第 2問: womanの複数形は women(y/n)? n 不正解。正解は yesでした。 第 3問: 火曜日を表す英単語は(Wednesday,Tuesday,Thursday,Monday,Friday)? Sunday 火曜日を表す英単語は(Wednesday,Tuesday,Thursday,Monday,Friday)? Tuesday 正解! 第 4問: 月を表す英単語は(正解は 4文字)? star 不正解。正解は moonでした。 第 5問: forgetの過去形は forgetted(y/n)? y 不正解。正解は noでした。 第 6問: childの複数形は(childs,childes,child,children)? childs 不正解。正解は childrenでした。 第 7問: りんごを表す英単語は? apple 正解! 第 8問: みかんを表す英単語は? orange 正解! ------------ 8問中 4問正解でした。
問題形式がばらばらなのに、メインメソッドでは switch文を使用していません。多態性で実現させています。
ソースコードの中に、新しい文法的な要素はほとんどありません。今までの知識でそのほとんどすべてを理解可能です。ただ、簡単なプログラムではありませんので、じっくりと考えて理解すると勉強になると思います。
まずは、自分で問題を追加したり、編集したりすると楽しめると思います。