多態性(ポリモーフィズム)


基本的な考え方

継承のis-aの考え方を使って、インスタンスの大枠をプログラム上で認識させる。
例えば、トラッククラスから生成された、トラックインスタンスを、自動車クラスのインスタンスとしてプログラム上に認識させるための方法。

インスタンスの大枠をプログラムで認識させる方法

自動車・トラックの例でいえば・・・

Car car =new Truck

①new Truck
インスタンスを生成する。

②Car car
生成したインスタンスを何とみなすか。今回「トラッククラス」から生成したインスタンスだが、「自動車」としてみなすという意味になる。

イメージ



親クラスに対して、子クラスから生成されたインスタンスを代入する

考え方

親クラスに対して、子クラスから生成されたインスタンスを代入すると、子クラスのメンバは使用出来なくなる。

イメージ

親クラスと子クラスに同じ名前のメソッドがある場合の動き

考え方

継承の考え方により、親クラスのメソッドを呼び出した際も、子クラスのメソッドが呼び出される。

イメージ

・上記図はWizardとしてみなすWizardインスタンスと、CharacterとしてみなすWizardインスタンスの2種類がある。
・それぞれのメソッドrunを呼び出しているが、CharacterとしてみなすWizardインスタンスにて、メソッドrunを呼び出した場合、Characterクラスのメソッドが呼び出されるかと思いきや・・・
・継承の考え方で子クラスのメソッドが優先的に呼び出されるため、Wizardクラスのインスタンスが呼び出される。

インスタンスの大枠を、親クラス→子クラスに変更したい時

考え方

上記例に置き換えると、Characterとしてみなす、wizardインスタンスを、Wizardとしてみなすwizardインスタンスに変更したい時の方法。

変更方法

普通に考えると・・・

Wizard wizard2 = character;

で良さそうだけど、これだとエラーがでる・・・。
理由は、Characterとしてみなす、wizardインスタンスはこれまでCharacterという、大枠で捉えてきた。そのため、コンピュータからすると「これって本当にwizardなの??Characterって他にもいるよね??」ってなる。

そのため

Wizard wizard2 = (Wizard)character;

キャスト演算子を用いて、強制的にクラスを変更しなければならない。上記でいうと、(Wizard)型の変数に強制的に変更している。

しかし

Character character = new Hero;//ヒーローはキャラクターとして捉える!
Wizard wizard2 = (Wizard)character;//ヒーローは魔法使いとして捉えろ!(??)

上記のようなミスが発生する可能性もある。
・最初キャラクターとして捉える、ヒーローインスタンスを生成。
・魔法使いとして捉える、ヒーローインスタンスを生成。
これだと、Is-aの関係が成り立たないため、ClassCastException(キャストによる強制変更が間違っている)というエラーになってしまう。
 ・○ ヒーローはキャラクターです。
 ・× ヒーローは魔法使いです。

つまり、キャストで強制変更出来るクラスのは、インスタンスを生成した際に使用したクラスまたは、その子クラス以降のクラスのみである。

キャストによる強制変更が正しいか判断する方法

方法

キャストによる強制変換が正しいか判別するために、javaではinstanceof演算子を使用する。
instanceof演算子は、キャスト演算子で指定された型に代入しても、矛盾しないかを判別する。

if(character instanceof Wizard){//もし、`character`を `Wizard`としてみなして良いなら
 Wizard wizard2 = (Wizard)character;
}

多様性のメリット

処理をまとめる事が出来る

考え方

インスタンス生成時に、クラス変数に配列を使用する事で、インスタンスに対してまとめた処理が出来る。

配列を使わなかったら
ヒーロークラスからHero1Hero2Hero3、魔法使いクラスからWizard1Wizard2Wizard3をそれぞれ生成して、全員に攻撃させたい場合

//インスタンスの生成
Hero hero1 = new Hero();
Hero hero2 = new Hero();
Hero hero3 = new Hero();
Wizard wizard1 = new Wizard();
Wizard wizard2 = new Wizard();
Wizard wizard3 = new Wizard();

//メソッドの呼び出し
hero1.attack();
hero2.attack();
hero3.attack();
wizard1.attack();
wizard1.attack();
wizard1.attack();

これだとメソッドの呼び出しを同じ様な処理を繰り返してて、いまいちイケてない・・・。

配列を使うと

//配列の作成
Character[] character = new Character[6];

//インスタンスの生成
Character character[0] = new Hero;
Character character[1] = new Hero;
Character character[2] = new Hero;
Character character[3] = new Wizard;
Character character[4] = new Wizard;
Character character[5] = new Wizard;

//メソッドの呼び出し
for(Character ch : character){
 ch.attack();
}

メソッドの処理がスッキリまとまって、いい感じ!

引数をざっくり受け取る事が出来る

モンスターに攻撃したい場合

    public static void main(String[] args) {
        //配列変数の作成
        Monster[] monster = new Monster[4];
        //モンスターインスタンスの生成
        monster[0] = new Slime();
        monster[1] = new Slime();
        monster[2] = new Goblin();
        monster[3] = new Goblin();
        //ヒーローインスタンスの生成
        Hero hero = new Hero();
        //モンスターに攻撃
        hero.attack(monster[0]);
    }

public class Hero {
    void attack(Monster monster) {
        monster.hp-=10;
    }
}

こうする事で、Heroクラスのattackメソッドの引数を、モンスターの種類毎に用意する必要が無くなる。

異なる処理結果の処理をまとめる事が出来る

考え方

親クラスのクラス名でメソッドを呼び出しても、子クラスのメソッドが呼び出されるため、メソッドの呼び出しをまとめる事が出来る

スライムクラス

public class Slime extends Monster {
    public void run() {
        System.out.println("スライムはうねうね逃げ出した!");
    }
}

ゴブリンクラス

public class Goblin extends Monster{
    public void run() {
        System.out.println("ゴブリンはドタバタ逃げ出した!");
    }
}

モンスタークラス


public abstract class Monster {
    String name;
    int hp = 50;
    public abstract void run();
}

メインメソッド

//モンスターインスタンスの生成
monster[0] = new Slime();
monster[1] = new Goblin();
//モンスタークラスでメソッドを呼び出し
for(Monster mo:monster) {
mo.run();

出力結果


スライムはうねうね逃げ出した
ゴブリンはドタバタ逃げ出した

モンスタークラスで呼び出しているが、それぞれ中身のクラスのメソッド内容が実行される。
この際、必ず親メソッドにも呼び出すメソッドをabstrackで定義する必要がある。