Java道|スーパークラス型とポリモーフィズムの理解

同じ命令でも、動く技は隊士ごとに変わる。
ポリモーフィズムを理解すると、親クラス型でまとめても、子クラスの個性がきちんと生きる理由が見えてきます。

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

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

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

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

鬼滅の刃風にたとえると、PillarSlayer は柱として特別な隊士ですが、まず DemonSlayer の一種でもあります。
そのため、DemonSlayer 型の変数で PillarSlayer オブジェクトを指すことができます。

DemonSlayer slayer1;
slayer1 = new PillarSlayer();

このとき、変数の型は DemonSlayer ですが、実際に入っているオブジェクトは PillarSlayer です。

ここでおもしろいのが、show() のようにオーバーライドされたメソッドを呼び出したときの動きです。
変数の型がスーパークラスでも、実体がサブクラスなら、実体に合ったサブクラス側の show() が動きます。

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

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

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

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

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

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

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

理由は、PillarSlayer が DemonSlayer の一種だからです。

鬼滅の刃風にたとえると、柱は特別な隊士です。
しかし、柱である前に、鬼殺隊士でもあります。

つまり、

関係意味
PillarSlayer は DemonSlayer を継承している柱は鬼殺隊士の一種
PillarSlayer オブジェクトを DemonSlayer 型で扱える柱を鬼殺隊士としてまとめて扱える

ということです。

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

↓クリックすると拡大表示されます。

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

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

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

DemonSlayer slayer1;
slayer1 = new PillarSlayer();

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

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

見た目の型は DemonSlayer です。
しかし、中に入っている実体は PillarSlayer です。

鬼滅の刃風にたとえると、司令部の名簿では「鬼殺隊士」として扱っていても、実際に前に立っているのが「柱」なら、柱としての振る舞いを見せるということです。

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

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

今回の中心はここです。

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

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

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

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

鬼滅の刃風にたとえると、司令部が「情報を見せて」と同じ命令を出しても、一般隊士なら一般隊士らしい表示、柱なら柱らしい表示になります。

同じ show() という命令でも、相手の正体によって動きが変わる。
これがポリモーフィズムです。

鬼滅の刃風に考える多態性

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

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

鬼滅の刃風にたとえると、司令部が複数の隊士に対して、同じように「自己紹介して」と命じたとします。

すると、

実際の隊士show() の動き
一般隊士名前と階級を表示する
名前、階級、担当区域を表示する

というように、同じ show() でも表示内容が変わります。

命令の形は同じです。
けれども、実際に動く内容は相手によって変わります。

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

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

ファイル名:Sample5.java

class DemonSlayer
{
    protected String name;
    protected String rank;

    public DemonSlayer()
    {
        name = "隊士未登録";
        rank = "階級未設定";
        System.out.println("鬼殺隊士を作成しました。");
    }

    public void setSlayer(String n, String r)
    {
        name = n;
        rank = r;
        System.out.println("名前を" + name + "に、階級を" + rank + "にしました。");
    }

    public void show()
    {
        System.out.println("隊士の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
    }
}

class PillarSlayer extends DemonSlayer
{
    private int area;

    public PillarSlayer()
    {
        area = 0;
        System.out.println("柱クラスの隊士を作成しました。");
    }

    public void setArea(int a)
    {
        area = a;
        System.out.println("担当区域を" + area + "にしました。");
    }

    public void show()
    {
        System.out.println("柱の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
        System.out.println("担当区域は" + area + "です。");
    }
}

class Sample5
{
    public static void main(String[] args)
    {
        DemonSlayer slayer1;
        slayer1 = new PillarSlayer();

        slayer1.setSlayer("水月", "水柱");

        slayer1.show();
    }
}

実行結果

鬼殺隊士を作成しました。
柱クラスの隊士を作成しました。
名前を水月に、階級を水柱にしました。
柱の名前は水月です。
階級は水柱です。
担当区域は0です。

Sample5.java で注目するところ

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

DemonSlayer slayer1;
slayer1 = new PillarSlayer();

変数 slayer1 の型は DemonSlayer です。
しかし、実際に作っているオブジェクトは PillarSlayer です。

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

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

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

slayer1.show();

ここで動くのは DemonSlayer の show() ではありません。
実際に入っているオブジェクトが PillarSlayer なので、PillarSlayer の show() が呼ばれます。

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

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

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

Sample5.java では、slayer1 の型は DemonSlayer です。

DemonSlayer slayer1;

しかし、実体は PillarSlayer です。

slayer1 = new PillarSlayer();

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

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

鬼滅の刃風にたとえると、名簿上は「鬼殺隊士」として扱われています。
でも、実際に前に出てきたのが柱なら、柱としての情報を見せます。

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

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

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

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

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

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

public void setArea(int a)

しかし、変数 slayer1 の型は DemonSlayer です。

DemonSlayer slayer1;

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

slayer1.setArea(7);

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

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

呼び出し使えるか理由
slayer1.setSlayer("水月", "水柱")使えるDemonSlayer に定義されているから
slayer1.show()使えるDemonSlayer にあり、実体に応じてオーバーライドが働くから
slayer1.setArea(7)使えないDemonSlayer 型には setArea() がないから

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

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

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

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

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

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

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

クラス意味
DemonSlayer鬼殺隊士の基本クラス
PillarSlayer柱の隊士クラス
FlamePillarSlayer炎柱の隊士クラス
WaterPillarSlayer水柱の隊士クラス

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

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

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

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

↓クリックすると拡大表示されます。

この図が示していること

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

左側の変数カードは、見た目の型が DemonSlayer であることを示しています。
右側のオブジェクトカードは、実体が PillarSlayer であることを示しています。

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

この図から分かることは、次の2つです。

見るポイント意味
変数の型呼び出せるメソッドの範囲を決める
実体のクラスオーバーライドされたメソッドの実行内容を決める

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

ファイル名:Sample6.java

class DemonSlayer
{
    protected String name;
    protected String rank;

    public DemonSlayer()
    {
        name = "隊士未登録";
        rank = "階級未設定";
        System.out.println("鬼殺隊士を作成しました。");
    }

    public void setSlayer(String n, String r)
    {
        name = n;
        rank = r;
        System.out.println("名前を" + name + "に、階級を" + rank + "にしました。");
    }

    public void show()
    {
        System.out.println("隊士の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
    }
}

class PillarSlayer extends DemonSlayer
{
    private int area;

    public PillarSlayer()
    {
        area = 0;
        System.out.println("柱クラスの隊士を作成しました。");
    }

    public void setArea(int a)
    {
        area = a;
        System.out.println("担当区域を" + area + "にしました。");
    }

    public void show()
    {
        System.out.println("柱の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
        System.out.println("担当区域は" + area + "です。");
    }
}

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

        slayers[0] = new DemonSlayer();
        slayers[0].setSlayer("蒼真", "隊士");

        slayers[1] = new PillarSlayer();
        slayers[1].setSlayer("水月", "水柱");

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

実行結果

鬼殺隊士を作成しました。
名前を蒼真に、階級を隊士にしました。
鬼殺隊士を作成しました。
柱クラスの隊士を作成しました。
名前を水月に、階級を水柱にしました。
隊士の名前は蒼真です。
階級は隊士です。
柱の名前は水月です。
階級は水柱です。
担当区域は0です。

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

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

DemonSlayer[] slayers;
slayers = new DemonSlayer[2];

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

slayers[0] = new DemonSlayer();
slayers[1] = new PillarSlayer();

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

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

配列要素変数としての型実際のオブジェクト
slayers[0]DemonSlayerDemonSlayer
slayers[1]DemonSlayerPillarSlayer

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

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

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

slayers[i].show();

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

対象実体実行される show()
slayers[0]DemonSlayerDemonSlayer の show()
slayers[1]PillarSlayerPillarSlayer の show()

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

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

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

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

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

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

ポリモーフィズムがあると、呼び出し側は「show() を呼べばよい」と考えるだけで済みます。
実際にどう表示するかは、各クラスの show() に任せられます。

鬼滅の刃風にたとえると、司令部は「各隊士、情報を示せ」と命じるだけです。
一般隊士は一般隊士として、柱は柱として、それぞれの情報を表示します。

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

↓クリックすると拡大表示されます。

この図が示していること

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

配列の型は DemonSlayer[] です。
しかし、その中には DemonSlayer の実体だけでなく、DemonSlayer を継承した PillarSlayer の実体も入れられます。

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

slayers[i].show();

それでも、実際には、

実体動くメソッド
DemonSlayerDemonSlayer の show()
PillarSlayerPillarSlayer の show()

というように切り替わります。

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

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

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

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

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

Sample6.java では、DemonSlayer と PillarSlayer の両方を DemonSlayer[] に入れています。
そして、同じ slayers[i].show() を呼び出しています。

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

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

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

ポリモーフィズムの良さは、コードを短くできることだけではありません。
設計の見通しがよくなることも大きな利点です。

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

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

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

鬼滅の刃風にたとえると、司令部は全員を鬼殺隊士としてまとめて扱えます。
しかし、実際に show() を命じると、一般隊士は一般隊士として、柱は柱として、自分に合った情報を示します。

鬼滅の刃風にスーパークラス型とポリモーフィズムを整理する

DemonSlayer は、鬼殺隊士全体を表す共通の型です。
PillarSlayer は、その DemonSlayer を受け継いだ柱の型です。

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

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

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

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

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

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

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

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

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

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