Java入門|スーパークラス型で扱う多態性(ポリモーフィズム)のしくみ

見た目は同じ命令でも、動く技は戦士ごとに変わる。
ポリモーフィズムを知ると、スーパークラス型でまとめても、それぞれの個性がちゃんと生きる理由が見えてくる。

継承を学んでくると、サブクラスはスーパークラスの性質や機能を受け継ぎながら、自分だけの特徴を追加できることが見えてきます。さらにオーバーライドによって、同じメソッド名でもサブクラスらしい動きをさせられることもわかってきます。ここまで理解できると、次に大切になるのが、スーパークラス型でまとめて扱うという考え方です。

Javaでは、サブクラスのオブジェクトをサブクラス型の変数で扱うだけでなく、スーパークラス型の変数でも扱えます。これは、サブクラスのオブジェクトが、同時にスーパークラスの一種でもあるからです。たとえばドラゴンボールで考えるなら、エリートサイヤ人は特別な戦士ですが、まずサイヤ人の一種でもあります。このため、サイヤ人型の変数でエリートサイヤ人を指すことができるわけです。

そして、ここで本当におもしろいのがメソッド呼び出しの動きです。
変数の型がスーパークラスでも、実際に入っているオブジェクトがサブクラスなら、そのオブジェクトに合ったメソッドが呼び出されます。つまり、見た目は同じ show() でも、中で動く処理は悟空なのか、ベジータなのか、悟飯なのかで変わる感覚です。これが多態性、つまりポリモーフィズムの大事な入り口になります。

今回は「スーパークラス型で扱う多態性のしくみ」を、ドラゴンボールの戦士たちに置きかえながら、やさしく整理していきます。特に、

  • サブクラスのオブジェクトをスーパークラス型で扱える理由
  • スーパークラス型の変数から show() を呼んでも、サブクラス側が動く理由
  • サブクラス独自メソッドはそのまま呼べないこと
  • 配列でまとめて扱うとポリモーフィズムの良さがよく見えること

を順番に見ていきます。

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

まず大事なところから見ていきます。
Javaでは、サブクラスのオブジェクトをスーパークラス型の変数で扱えます。

ドラゴンボール風に表すと、次の2つはどちらも書けるということです。

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

なぜこれができるのかというと、エリートサイヤ人はサイヤ人の一種だからです。
サブクラスはスーパークラスを継承しているので、サブクラスのオブジェクトは「子クラスのオブジェクト」であると同時に、「親クラスとして見ても成立するオブジェクト」でもあります。

ドラゴンボールで言えば、

  • エリートサイヤ人は特別任務を持つ戦士
  • でもまずサイヤ人である

という関係です。

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

変数の型と実際のオブジェクトは別に考える

ここはポリモーフィズムを理解するうえでとても大事です。

Javaでは、変数の型と、実際にそこへ入っているオブジェクトのクラスを分けて考えます。

たとえば次のようなコードがあるとします。

Saiyan warrior1;
warrior1 = new EliteSaiyan();

このとき、

  • 変数 warrior1 の型は Saiyan
  • 実際に入っているオブジェクトは EliteSaiyan

です。

見た目の型は親ですが、中身は子です。
そしてメソッド呼び出しでは、この「中身が何者か」がとても重要になります。

ドラゴンボールでたとえると、界王様が「サイヤ人たちを並べろ」と言って集めたとしても、その中にいるのがベジータなら、実際に動くのはベジータらしい振る舞いになります。
ラベルがサイヤ人でも、実体がベジータなら、ベジータとして動くわけです。

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

ここが今回の中心です。

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

つまり、

  • 変数の見た目が Saiyan
  • 実体が EliteSaiyan

なら、show() を呼び出したときには EliteSaiyan の show() が使われます。

これは、Javaが
変数の型ではなく、実際のオブジェクトのクラスを見て、適切なメソッドを選ぶ
しくみになっているからです。

この感覚がつかめると、ポリモーフィズムがかなりわかりやすくなります。

ドラゴンボールで考える多態性の感覚

ドラゴンボールでたとえると、たとえば「戦士として自己紹介しろ」という同じ命令を出したとしても、

  • 悟空なら悟空らしい紹介
  • ベジータならベジータらしい紹介
  • 悟飯なら悟飯らしい紹介

になります。

命令の名前は同じです。
でも、実際の振る舞いはオブジェクトごとに変わります。

これが多態性です。

つまりポリモーフィズムは、
同じ呼び出し方なのに、相手の正体に応じて中身が切り替わるしくみ
だと考えると自然です。

サンプルプログラム:ポリモーフィズムの確認

ここでは、スーパークラス型の変数でサブクラスのオブジェクトを扱いながら、show() の呼び出しがどう動くかを確認できるドラゴンボール風のサンプルを見ていきます。スーパークラス Saiyan とサブクラス EliteSaiyan の関係を使います。

ファイル名:Sample5.java

class Saiyan
{
    protected String name;
    protected int power;

    public Saiyan()
    {
        name = "戦士なし";
        power = 0;
        System.out.println("サイヤ人を作成しました。");
    }

    public void setSaiyan(String n, int p)
    {
        name = n;
        power = p;
        System.out.println("名前を" + name + "に戦闘力を" + power + "にしました。");
    }

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

class EliteSaiyan extends Saiyan
{
    private int area;

    public EliteSaiyan()
    {
        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("戦闘力は" + power + "です。");
        System.out.println("担当エリアは" + area + "です。");
    }
}

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

        warrior1.setSaiyan("ベジータ", 18000);

        warrior1.show();
    }
}

実行結果

サイヤ人を作成しました。
エリートサイヤ人を作成しました。
名前をベジータに戦闘力を18000にしました。
エリートサイヤ人の名前はベジータです。
戦闘力は18000です。
担当エリアは0です。

このプログラムで見てほしいところ

このプログラムでは、変数 warrior1 の型は Saiyan です。
けれども、実際に代入しているのは new EliteSaiyan() です。

つまり、整理するとこうなります。

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

そしてこの状態で warrior1.show(); を呼び出すと、動くのは Saiyan の show() ではなく、EliteSaiyan の show() です。

ここがとても大切です。

見た目はスーパークラス型でも、実体がエリートサイヤ人なのだから、表示もエリートサイヤ人版になる。
このしくみが、オーバーライドとポリモーフィズムのつながりです。

なぜ EliteSaiyan の show が呼ばれるのか

理由は、Javaのメソッド呼び出しが、オブジェクト自身のクラスに基づいて行われるからです。

つまり warrior1 という変数が Saiyan 型であっても、そこに入っている実体が EliteSaiyan なら、show() の呼び出しでは EliteSaiyan 側が選ばれます。

この動きは、オーバーライドされたメソッドに対して起こります。

ドラゴンボールで言えば、「サイヤ人の紹介をしろ」と命令したとしても、前に出てきたのがエリート戦士なら、その紹介はエリート戦士らしいものになります。
ラベルだけではなく、実際に前に立っている人物に合わせて動くわけです。

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

ここはかなり重要な注意点です。

スーパークラス型の変数でサブクラスのオブジェクトを扱うと、オーバーライドされた共通メソッドはきれいに呼び出せます。
でも、サブクラスで新しく追加した独自メソッドは、そのままでは呼び出せません。

たとえば今回の setArea() は EliteSaiyan 独自のメソッドです。
変数 warrior1 の型は Saiyan なので、次のような呼び出しはできません。

warrior1.setArea(7);

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

この点を表で整理すると、こうなります。

呼び出し使えるか理由
warrior1.setSaiyan("ベジータ", 18000)使えるSaiyan に定義されているから
warrior1.show()使えるSaiyan にあり、しかも実体に応じてオーバーライドが働くから
warrior1.setArea(7)使えないSaiyan 型にはそのメソッドがないから

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

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

ここまで見ると、わざわざスーパークラス型で扱う意味は何だろうと感じるかもしれません。
でも、この考え方があるからこそ、いろいろな種類のオブジェクトをまとめて扱えるようになります。

たとえば、

  • Saiyan
  • EliteSaiyan
  • ほかのサイヤ人派生クラス

のように、共通の親を持つオブジェクトたちを、同じ Saiyan 型で並べられるようになります。

すると、共通メソッド show() を同じ書き方で呼び出すだけで、それぞれに合った処理が自動で動きます。

ここにポリモーフィズムの強さがあります。

サンプルプログラム:スーパークラス型の配列

次は、スーパークラス型の配列を使って、スーパークラスのオブジェクトとサブクラスのオブジェクトを一緒に扱う例です。ここまで来ると、ポリモーフィズムの便利さがかなりはっきり見えてきます。

ファイル名:Sample6.java

class Saiyan
{
    protected String name;
    protected int power;

    public Saiyan()
    {
        name = "戦士なし";
        power = 0;
        System.out.println("サイヤ人を作成しました。");
    }

    public void setSaiyan(String n, int p)
    {
        name = n;
        power = p;
        System.out.println("名前を" + name + "に戦闘力を" + power + "にしました。");
    }

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

class EliteSaiyan extends Saiyan
{
    private int area;

    public EliteSaiyan()
    {
        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("戦闘力は" + power + "です。");
        System.out.println("担当エリアは" + area + "です。");
    }
}

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

        warriors[0] = new Saiyan();
        warriors[0].setSaiyan("悟空", 9000);

        warriors[1] = new EliteSaiyan();
        warriors[1].setSaiyan("ベジータ", 18000);

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

実行結果

サイヤ人を作成しました。
名前を悟空に戦闘力を9000にしました。
サイヤ人を作成しました。
エリートサイヤ人を作成しました。
名前をベジータに戦闘力を18000にしました。
戦士の名前は悟空です。
戦闘力は9000です。
エリートサイヤ人の名前はベジータです。
戦闘力は18000です。
担当エリアは0です。

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

このプログラムでは、Saiyan[] という配列を用意して、そこへ

  • Saiyan のオブジェクト
  • EliteSaiyan のオブジェクト

を一緒に入れています。

これは、どちらも Saiyan の一種だからできることです。

そしてループの中では、ただ1行、

warriors[i].show();

と呼び出しているだけです。

それなのに、

  • warriors[0] が Saiyan なら Saiyan の show()
  • warriors[1] が EliteSaiyan なら EliteSaiyan の show()

が動きます。

これこそがポリモーフィズムの気持ちよさです。

同じ show() という呼び出しで、相手に応じた適切な表示が行われる。
コードを書く側は、細かく場合分けをしなくて済みます。

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

もしこのしくみがなければ、オブジェクトごとに

  • これは Saiyan か
  • これは EliteSaiyan か

を毎回チェックしながら、別々のメソッドを呼び分けなければならなくなります。

でもポリモーフィズムがあると、共通のメソッド名 show() を1つ決めておくだけでよくなります。

ポリモーフィズムがない場合ポリモーフィズムがある場合
クラスごとに場合分けが増える同じ show() でまとめて呼べる
メソッド名を個別に覚える必要がある共通の名前で扱える
コードが長くなりやすいコードが整理しやすい

この違いはかなり大きいです。

だからオーバーライドは、単に親のメソッドを上書きするだけの仕組みではなく、いろいろなオブジェクトをまとめて扱いやすくするための土台でもあるわけです。

図で多態性の流れをつかむ

この図では、まず左上で Saiyan 型の変数が EliteSaiyan オブジェクトを指しているところを見ます。
ここから、見た目の型は親でも、実体は子クラスになれることがわかります。

次に左下では、Saiyan 配列に親クラスと子クラスの両方が入っている様子を見ます。
そして下部の warriors[i].show() という1つの呼び出しが、それぞれのオブジェクトに合った処理へつながっているところがポイントです。

つまりこの図が表しているのは、まとめて扱っても、個性は失われない ということです。
これが多態性のいちばん大事な感覚です。

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

ここで改めて整理すると、ポリモーフィズムはオーバーライドと深くつながっています。

流れとしてはこうです。

  1. スーパークラスに共通のメソッドがある
  2. サブクラスでそのメソッドをオーバーライドする
  3. スーパークラス型でいろいろなオブジェクトをまとめて扱う
  4. 同じメソッド呼び出しでも、実体に応じて適切な処理が動く

この 4 つの流れがそろうことで、多態性がはっきり見えてきます。

ドラゴンボールで言えば、

  • 戦士たちはみんな show() という共通の見せ方を持つ
  • でも中身は悟空、ベジータ、悟飯で違う
  • それでも「戦士たちを並べて show を呼ぶ」だけで、それぞれらしい動きになる

という状態です。

多態性は「まとめやすさ」と「わかりやすさ」を生む

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

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

そして実際の内容は、それぞれのクラスが責任を持って定義します。

この分担があるので、

  • 呼び出す側は共通の名前だけ知っていればよい
  • 実際の表示のしかたは各クラスに任せられる

という、とてもきれいな設計になります。

ドラゴンボールで感覚的に整理する

最後に、ドラゴンボールの感覚で整理してみましょう。

Saiyan は戦士全体の共通の型です。
EliteSaiyan はその中の特別な戦士です。
だから、エリートサイヤ人をサイヤ人として扱うことができます。

でも、サイヤ人として扱ったからといって、エリート戦士らしさが消えるわけではありません。
show() を呼べば、ちゃんとエリートサイヤ人らしい show() が動きます。

つまり、

  • まとめて扱うために親クラス型を使う
  • それでも実際の振る舞いは実体のクラスに従う

というのが、多態性の核心です。

ひとことで言うなら、
同じ命令で呼び出しても、前に立っている戦士に合わせて技が変わる
これがスーパークラス型で扱うポリモーフィズムのいちばん大事な感覚です。