以下のようなプログラムについて考えます。
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文を使用していません。多態性で実現させています。
ソースコードの中に、新しい文法的な要素はほとんどありません。今までの知識でそのほとんどすべてを理解可能です。ただ、簡単なプログラムではありませんので、じっくりと考えて理解すると勉強になると思います。
まずは、自分で問題を追加したり、編集したりすると楽しめると思います。