Java超|スーパークラス型とポリモーフィズム

同じ show() という命令でも、普通のサイヤ人戦士とスーパーサイヤ人戦士では動きが変わります。ポリモーフィズムを理解すると、親クラス型でまとめても、子クラスの個性が消えない理由が見えてきます。

継承を学ぶと、サブクラスはスーパークラスの性質を受け継ぎながら、自分だけの特徴を追加できることが分かります。

さらに、オーバーライドを使うと、スーパークラスにあるメソッドをサブクラス側で自分用に作り直せます。

ここまで理解できると、次に大切になるのが、スーパークラス型でサブクラスのオブジェクトを扱うという考え方です。

Javaでは、サブクラスのオブジェクトをサブクラス型の変数で扱うだけでなく、スーパークラス型の変数でも扱えます。

ドラゴンボールの世界観でたとえると、SuperSaiyanWarrior はスーパーサイヤ人として特別な戦士です。

しかし、スーパーサイヤ人である前に、SaiyanWarrior の一種でもあります。

そのため、SaiyanWarrior 型の変数で SuperSaiyanWarrior オブジェクトを指すことができます。

SaiyanWarrior warrior1;
warrior1 = new SuperSaiyanWarrior();

このとき、変数の型は SaiyanWarrior ですが、実際に作られているオブジェクトは SuperSaiyanWarrior です。

ここでおもしろいのが、show() のようにオーバーライドされたメソッドを呼び出したときの動きです。

変数の型がスーパークラスでも、実体がサブクラスなら、実体に合ったサブクラス側の show() が動きます。

これがポリモーフィズム、つまり多態性の大切な考え方です。

同じ命令でも、実際に立っている戦士によって動く技が変わる。

この感覚がつかめると、Javaのオブジェクト指向がかなり実践的に見えてきます。この記事では、具体的なプログラムとして Sample5.java と Sample6.java を例示しながら、スーパークラス型でサブクラスのオブジェクトを扱う方法、変数の型と実体の違い、オーバーライドされた show() の呼び出し、配列でまとめたときのポリモーフィズムを、ドラゴンボールの世界観で解説します。

サブクラスのオブジェクトはスーパークラス型でも扱える

まず大切なのは、サブクラスのオブジェクトをスーパークラス型の変数に入れられるという点です。

たとえば、SaiyanWarrior というスーパークラスと、それを継承した SuperSaiyanWarrior というサブクラスがあるとします。

このとき、次の2つの書き方はどちらもできます。

書き方意味
SuperSaiyanWarrior warrior1 = new SuperSaiyanWarrior();サブクラス型の変数で、サブクラスのオブジェクトを扱う
SaiyanWarrior warrior1 = new SuperSaiyanWarrior();スーパークラス型の変数で、サブクラスのオブジェクトを扱う

なぜこのような書き方ができるのでしょうか。

理由は、SuperSaiyanWarrior が SaiyanWarrior の一種だからです。

ドラゴンボール風にたとえると、スーパーサイヤ人戦士は特別な戦士です。

しかし、スーパーサイヤ人戦士である前に、サイヤ人戦士でもあります。

つまり、次の関係です。

関係意味
SuperSaiyanWarrior は SaiyanWarrior を継承しているスーパーサイヤ人戦士はサイヤ人戦士の一種
SuperSaiyanWarrior オブジェクトを SaiyanWarrior 型で扱えるスーパーサイヤ人戦士をサイヤ人戦士としてまとめて扱える

この「子クラスは親クラスの一種である」という関係が、スーパークラス型で扱える理由です。

変数の型と実際のオブジェクトは分けて考える

ポリモーフィズムを理解するうえで、とても大切なのが、変数の型と実際のオブジェクトのクラスを分けて考えることです。

たとえば、次のコードを見てください。

SaiyanWarrior warrior1;
warrior1 = new SuperSaiyanWarrior();

この場合、整理すると次のようになります。

見るポイント内容
変数 warrior1 の型SaiyanWarrior
実際に作られたオブジェクトSuperSaiyanWarrior

見た目の型は SaiyanWarrior です。

しかし、中に入っている実体は SuperSaiyanWarrior です。

ドラゴンボール風にたとえると、戦士管理本部の名簿では「サイヤ人戦士」として扱っていても、実際に前に立っているのが「スーパーサイヤ人戦士」なら、スーパーサイヤ人としての振る舞いを見せるということです。

ここがポリモーフィズムの出発点です。

見るもの決まること
変数の型呼び出せるメソッドの範囲
実際のオブジェクトオーバーライドされたメソッドの実行内容

この2つを分けて見ると、スーパークラス型とポリモーフィズムの動きがかなり整理しやすくなります。

スーパークラス型で扱ってもサブクラスの show() が呼ばれる

今回の中心はここです。

スーパークラス型の変数でサブクラスのオブジェクトを扱っていても、オーバーライドされたメソッドを呼び出すと、実際のオブジェクトのクラスに応じたメソッドが動きます。

たとえば、変数の型が SaiyanWarrior でも、実体が SuperSaiyanWarrior なら、show() を呼び出したときには SuperSaiyanWarrior の show() が呼ばれます。

変数の型実際のオブジェクトshow() で動くもの
SaiyanWarriorSaiyanWarriorSaiyanWarrior の show()
SaiyanWarriorSuperSaiyanWarriorSuperSaiyanWarrior の show()

これは、Javaがオーバーライドされたメソッドについて、実際のオブジェクトのクラスを見て呼び出すメソッドを決めるからです。

ドラゴンボール風にたとえると、戦士管理本部が「情報を見せて」と同じ命令を出しても、普通のサイヤ人戦士なら普通の戦士らしい表示、スーパーサイヤ人戦士ならスーパーサイヤ人らしい表示になります。

同じ show() という命令でも、相手の正体によって動きが変わる。

これがポリモーフィズムです。

図:スーパークラス型でサブクラスを指すイメージ

この図が示していること

この図では、SaiyanWarrior 型の変数 warrior1 が、SuperSaiyanWarrior オブジェクトを指している様子を表しています。

左側の変数カードは、見た目の型が SaiyanWarrior であることを示しています。

右側のオブジェクトカードは、実体が SuperSaiyanWarrior であることを示しています。

ここで warrior1.show() を呼び出すと、変数の型だけを見るのではなく、実体である SuperSaiyanWarrior の show() が動きます。

この図から分かることは、変数の型と実際のオブジェクトのクラスは分けて考える必要があるということです。

ドラゴンボール風に考える多態性

ポリモーフィズムは、日本語では多態性と呼ばれます。

多態性とは、簡単にいうと、同じ呼び出し方でも、実際のオブジェクトによって動きが変わることです。

ドラゴンボール風にたとえると、戦士管理本部が複数の戦士に対して、同じように「自己紹介して」と命じたとします。

すると、次のように動きが変わります。

実際の戦士show() の動き
普通のサイヤ人戦士名前と流派を表示する
スーパーサイヤ人戦士名前、流派、変身段階を表示する

命令の形は同じです。

けれども、実際に動く内容は相手によって変わります。

これが、ポリモーフィズムの感覚です。

同じ show() という命令を出しても、前に立っている戦士が誰なのかによって、表示される内容が変わります。

この仕組みがあるから、複数種類のオブジェクトを同じ親クラス型でまとめて扱いやすくなります。

ポリモーフィズムの動作確認

ファイル名:Sample5.java

class SaiyanWarrior
{
    protected String name;
    protected String styleName;

    public SaiyanWarrior()
    {
        name = "戦士未登録";
        styleName = "流派未設定";
        System.out.println("サイヤ人戦士を作成しました。");
    }

    public void setWarrior(String n, String s)
    {
        name = n;
        styleName = s;
        System.out.println("名前を" + name + "に、流派を" + styleName + "にしました。");
    }

    public void show()
    {
        System.out.println("戦士の名前は" + name + "です。");
        System.out.println("流派は" + styleName + "です。");
    }
}

class SuperSaiyanWarrior extends SaiyanWarrior
{
    private int transformationLevel;

    public SuperSaiyanWarrior()
    {
        transformationLevel = 0;
        System.out.println("スーパーサイヤ人クラスの戦士を作成しました。");
    }

    public void setTransformationLevel(int level)
    {
        transformationLevel = level;
        System.out.println("変身段階を" + transformationLevel + "にしました。");
    }

    public void show()
    {
        System.out.println("スーパーサイヤ人の名前は" + name + "です。");
        System.out.println("流派は" + styleName + "です。");
        System.out.println("変身段階は" + transformationLevel + "です。");
    }
}

class Sample5
{
    public static void main(String[] args)
    {
        SaiyanWarrior warrior1;
        warrior1 = new SuperSaiyanWarrior();

        warrior1.setWarrior("悟空", "亀仙流");

        warrior1.show();
    }
}

Sample5.java の実行結果

サイヤ人戦士を作成しました。
スーパーサイヤ人クラスの戦士を作成しました。
名前を悟空に、流派を亀仙流にしました。
スーパーサイヤ人の名前は悟空です。
流派は亀仙流です。
変身段階は0です。

Sample5.java の注目点

このプログラムで特に見てほしいのは、次の部分です。

SaiyanWarrior warrior1;
warrior1 = new SuperSaiyanWarrior();

変数 warrior1 の型は SaiyanWarrior です。

しかし、実際に作っているオブジェクトは SuperSaiyanWarrior です。

整理すると、次のようになります。

項目内容
変数の型SaiyanWarrior
実際のオブジェクトSuperSaiyanWarrior

この状態で、次のように show() を呼び出しています。

warrior1.show();

ここで動くのは SaiyanWarrior の show() ではありません。

実際に入っているオブジェクトが SuperSaiyanWarrior なので、SuperSaiyanWarrior の show() が呼ばれます。

つまり、変数の型は親でも、実体が子なら、オーバーライドされたメソッドは子クラス版が動くということです。

なぜ SuperSaiyanWarrior の show() が呼ばれるのか

理由は、Javaがオーバーライドされたメソッドを呼び出すとき、実際のオブジェクトのクラスを見て判断するからです。

Sample5.java では、warrior1 の型は SaiyanWarrior です。

SaiyanWarrior warrior1;

しかし、実体は SuperSaiyanWarrior です。

warrior1 = new SuperSaiyanWarrior();

そのため、show() を呼び出すと、SuperSaiyanWarrior の show() が実行されます。

呼び出し変数の型実体実行されるメソッド
warrior1.show()SaiyanWarriorSuperSaiyanWarriorSuperSaiyanWarrior の show()

ドラゴンボール風にたとえると、名簿上はサイヤ人戦士として扱われています。

でも、実際に前に出てきたのがスーパーサイヤ人戦士なら、スーパーサイヤ人としての情報を見せます。

これが、ポリモーフィズムの大切な動きです。

ただしサブクラス独自のメソッドはそのまま呼べない

ここはとても重要な注意点です。

スーパークラス型の変数でサブクラスのオブジェクトを扱うと、オーバーライドされた共通メソッドは実体に応じて動きます。

しかし、サブクラスで新しく追加した独自メソッドは、そのままでは呼び出せません。

Sample5.java では、setTransformationLevel() は SuperSaiyanWarrior 独自のメソッドです。

public void setTransformationLevel(int level)

しかし、変数 warrior1 の型は SaiyanWarrior です。

SaiyanWarrior warrior1;

そのため、次のようには書けません。

warrior1.setTransformationLevel(1);

なぜなら、SaiyanWarrior 型として見ると、setTransformationLevel() というメソッドは存在しないからです。

この違いを表にすると、次のようになります。

呼び出し使えるか理由
warrior1.setWarrior("悟空", "亀仙流")使えるSaiyanWarrior に定義されているから
warrior1.show()使えるSaiyanWarrior にあり、実体に応じてオーバーライドが働くから
warrior1.setTransformationLevel(1)使えないSaiyanWarrior 型には setTransformationLevel() がないから

ここで押さえておきたい考え方は、次の2つです。

見るポイント決まること
変数の型呼び出せるメソッドが決まる
実際のオブジェクトオーバーライドされたメソッドの中身が決まる

つまり、何を呼び出せるかは変数の型で決まり、実際にどの処理が動くかはオブジェクトの実体で決まると考えると整理しやすいです。

スーパークラス型で扱う意味

では、なぜわざわざスーパークラス型で扱うのでしょうか。

その理由は、いろいろなサブクラスのオブジェクトを、同じ親クラス型でまとめて扱えるからです。

たとえば、SaiyanWarrior を親クラスとして、次のようなクラスがあるとします。

クラス意味
SaiyanWarriorサイヤ人戦士の基本クラス
SuperSaiyanWarriorスーパーサイヤ人戦士クラス
GodSaiyanWarrior神流の覚醒戦士クラス
PlanetEliteWarrior惑星戦士流の精鋭戦士クラス

これらをすべて SaiyanWarrior 型として扱えれば、同じ配列や同じループで処理しやすくなります。

そして show() を共通メソッドとして用意しておけば、呼び出し側は同じように show() と書くだけで、それぞれのクラスに合った表示が行われます。

これが、ポリモーフィズムの便利なところです。

図:変数の型と実体で決まること

この図が示していること

この図では、SaiyanWarrior 型の変数 warrior1 が、SuperSaiyanWarrior オブジェクトを指しているときの動きを表しています。

変数の型は SaiyanWarrior なので、呼び出せるメソッドは SaiyanWarrior に定義されている範囲になります。

そのため、setWarrior() や show() は呼び出せます。

一方、setTransformationLevel() は SuperSaiyanWarrior 独自のメソッドなので、SaiyanWarrior 型の変数からはそのまま呼び出せません。

しかし、show() は SaiyanWarrior にあり、SuperSaiyanWarrior でオーバーライドされています。

そのため、warrior1.show() を呼ぶと、実体である SuperSaiyanWarrior の show() が動きます。

この図から分かるのは、呼び出せるメソッドの範囲は変数の型で決まり、オーバーライドされたメソッドの実行内容は実体のクラスで決まるということです。

オブジェクト配列でポリモーフィズムを確認する

ファイル名:Sample6.java

class SaiyanWarrior
{
    protected String name;
    protected String styleName;

    public SaiyanWarrior()
    {
        name = "戦士未登録";
        styleName = "流派未設定";
        System.out.println("サイヤ人戦士を作成しました。");
    }

    public void setWarrior(String n, String s)
    {
        name = n;
        styleName = s;
        System.out.println("名前を" + name + "に、流派を" + styleName + "にしました。");
    }

    public void show()
    {
        System.out.println("戦士の名前は" + name + "です。");
        System.out.println("流派は" + styleName + "です。");
    }
}

class SuperSaiyanWarrior extends SaiyanWarrior
{
    private int transformationLevel;

    public SuperSaiyanWarrior()
    {
        transformationLevel = 0;
        System.out.println("スーパーサイヤ人クラスの戦士を作成しました。");
    }

    public void setTransformationLevel(int level)
    {
        transformationLevel = level;
        System.out.println("変身段階を" + transformationLevel + "にしました。");
    }

    public void show()
    {
        System.out.println("スーパーサイヤ人の名前は" + name + "です。");
        System.out.println("流派は" + styleName + "です。");
        System.out.println("変身段階は" + transformationLevel + "です。");
    }
}

class Sample6
{
    public static void main(String[] args)
    {
        SaiyanWarrior[] warriors;
        warriors = new SaiyanWarrior[2];

        warriors[0] = new SaiyanWarrior();
        warriors[0].setWarrior("天津飯", "鶴仙流");

        warriors[1] = new SuperSaiyanWarrior();
        warriors[1].setWarrior("悟空", "亀仙流");

        for(int i = 0; i < warriors.length; i++){
            warriors[i].show();
        }
    }
}

Sample6.java の実行結果

サイヤ人戦士を作成しました。
名前を天津飯に、流派を鶴仙流にしました。
サイヤ人戦士を作成しました。
スーパーサイヤ人クラスの戦士を作成しました。
名前を悟空に、流派を亀仙流にしました。
戦士の名前は天津飯です。
流派は鶴仙流です。
スーパーサイヤ人の名前は悟空です。
流派は亀仙流です。
変身段階は0です。

配列でまとめるとポリモーフィズムの良さがよく見える

Sample6.java では、SaiyanWarrior 型の配列を用意しています。

SaiyanWarrior[] warriors;
warriors = new SaiyanWarrior[2];

この配列には、SaiyanWarrior オブジェクトと SuperSaiyanWarrior オブジェクトの両方を入れています。

warriors[0] = new SaiyanWarrior();
warriors[1] = new SuperSaiyanWarrior();

これは、SuperSaiyanWarrior が SaiyanWarrior の一種だからできることです。

配列の中身を整理すると、次のようになります。

配列要素変数としての型実際のオブジェクト
warriors[0]SaiyanWarriorSaiyanWarrior
warriors[1]SaiyanWarriorSuperSaiyanWarrior

そして、ループでは次のように show() を呼び出しています。

for(int i = 0; i < warriors.length; i++){
    warriors[i].show();
}

呼び出し方は、どちらも同じです。

warriors[i].show();

しかし、実際に動く show() は、配列の中に入っているオブジェクトによって変わります。

対象実体実行される show()
warriors[0]SaiyanWarriorSaiyanWarrior の show()
warriors[1]SuperSaiyanWarriorSuperSaiyanWarrior の show()

これが、ポリモーフィズムの大きな魅力です。

同じ show() という呼び出しでも、相手に応じて適切な処理が自動で動きます。

もしポリモーフィズムがなかったら

もしポリモーフィズムがなければ、オブジェクトごとに種類を確認して、別々に処理を分ける必要があります。

たとえば、次のような考え方になってしまいます。

ポリモーフィズムがない場合ポリモーフィズムがある場合
これは普通の戦士か、スーパーサイヤ人戦士かを毎回確認するshow() を呼ぶだけでよい
クラスごとに処理を分ける実体に応じて自動で切り替わる
コードが長くなりやすいコードが整理しやすい
呼び出し側が細かい違いを知る必要がある違いは各クラスに任せられる

ポリモーフィズムがあると、呼び出し側は show() を呼べばよいと考えるだけで済みます。

実際にどう表示するかは、各クラスの show() に任せられます。

ドラゴンボール風にたとえると、戦士管理本部は「各戦士、情報を示せ」と命じるだけです。

普通のサイヤ人戦士は普通の戦士として、スーパーサイヤ人戦士はスーパーサイヤ人として、それぞれの情報を表示します。

図:配列でまとめても個性が生きるイメージ

この図が示していること

この図では、SaiyanWarrior[] 配列の中に、SaiyanWarrior オブジェクトと SuperSaiyanWarrior オブジェクトが一緒に入っている様子を表しています。

配列の型は SaiyanWarrior[] です。

しかし、その中には SaiyanWarrior の実体だけでなく、SaiyanWarrior を継承した SuperSaiyanWarrior の実体も入れられます。

そして、ループでは同じように show() を呼び出しています。

warriors[i].show();

それでも、実際には次のように切り替わります。

実体動くメソッド
SaiyanWarriorSaiyanWarrior の show()
SuperSaiyanWarriorSuperSaiyanWarrior の show()

この図から分かることは、まとめて扱っても、それぞれの個性は失われないということです。

オーバーライドとポリモーフィズムのつながり

ポリモーフィズムは、オーバーライドと深くつながっています。

流れとしては、次のように考えると分かりやすいです。

手順内容
1スーパークラスに共通メソッド show() を用意する
2サブクラスで show() をオーバーライドする
3スーパークラス型で複数のオブジェクトをまとめて扱う
4同じ show() 呼び出しで、実体に応じた処理が動く

Sample6.java では、SaiyanWarrior と SuperSaiyanWarrior の両方を SaiyanWarrior[] に入れています。

そして、同じ warriors[i].show() を呼び出しています。

それでも、実体が SaiyanWarrior なら SaiyanWarrior の show()、実体が SuperSaiyanWarrior なら SuperSaiyanWarrior の show() が動きます。

これが、オーバーライドとポリモーフィズムのつながりです。

多態性はまとめやすさと分かりやすさを生む

ポリモーフィズムの良さは、コードを短くできることだけではありません。

設計の見通しがよくなることも大きな利点です。

たとえば show() というメソッド名が共通なら、コードを読む人は、これは情報を表示する処理だとすぐに分かります。

そのうえで、具体的に何を表示するかは各クラスに任せられます。

呼び出し側各クラス側
show() を呼ぶだけでよい自分に合った show() を定義する
細かい種類を知らなくてよい自分の表示内容に責任を持つ
同じ配列やループで扱える実体ごとに違う処理を実行できる

ドラゴンボール風にたとえると、戦士管理本部は全員をサイヤ人戦士としてまとめて扱えます。

しかし、実際に show() を命じると、普通のサイヤ人戦士は普通のサイヤ人戦士として、スーパーサイヤ人戦士はスーパーサイヤ人戦士として、自分に合った情報を示します。

ドラゴンボール風にスーパークラス型とポリモーフィズムを整理する

SaiyanWarrior は、サイヤ人戦士全体を表す共通の型です。

SuperSaiyanWarrior は、その SaiyanWarrior を受け継いだスーパーサイヤ人戦士の型です。

そのため、SuperSaiyanWarrior のオブジェクトは SaiyanWarrior 型として扱えます。

しかし、SaiyanWarrior 型で扱ったからといって、SuperSaiyanWarrior らしさが消えるわけではありません。

show() を呼び出せば、実体が SuperSaiyanWarrior なら SuperSaiyanWarrior の show() が動きます。

Javaの考え方ドラゴンボール風のイメージ
スーパークラス型で扱うスーパーサイヤ人戦士をサイヤ人戦士として名簿に並べる
実体はサブクラス実際にはスーパーサイヤ人戦士として存在している
show() を呼ぶ情報を示せと命じる
サブクラス版が動くスーパーサイヤ人らしい情報表示をする

ひとことで言えば、ポリモーフィズムは、同じ命令で呼び出しても、前に立っている戦士に合わせて技が変わる仕組みです。

この内容で押さえておきたいポイント

ポイント内容
サブクラスのオブジェクトスーパークラス型の変数で扱える
理由サブクラスはスーパークラスの一種だから
変数の型呼び出せるメソッドの範囲を決める
実際のオブジェクトオーバーライドされたメソッドの実行内容を決める
show() の動き実体が SuperSaiyanWarrior なら SuperSaiyanWarrior の show() が動く
サブクラス独自メソッドスーパークラス型の変数からはそのまま呼べない
配列での利用SaiyanWarrior[] に SaiyanWarrior と SuperSaiyanWarrior をまとめられる
ポリモーフィズム同じ呼び出しで、実体ごとに違う処理が動く

スーパークラス型でまとめると、複数の種類のオブジェクトを同じ形で扱えるようになります。

そして、オーバーライドされたメソッドを呼び出せば、実際のオブジェクトに合った処理が動きます。

この「まとめて扱えるのに、個性は消えない」という感覚が、ポリモーフィズムの大切なポイントです。