Java超|共通部分をまとめる抽象クラス

未完成だからこそ、土台として強い。抽象クラスを理解すると、共通ルールを守りながら、戦士ごとの個性をきれいに育てられます。

Javaのオブジェクト指向では、複数のクラスに共通する情報や処理を、親クラスにまとめることができます。

これまでの継承では、SaiyanWarrior クラスを親クラスにして、SuperSaiyanWarrior クラスのような子クラスを作る考え方を学びました。

親クラスに共通部分をまとめておけば、子クラスでは必要な個性だけを追加できます。

たとえば、サイヤ人戦士なら、どの戦士にも共通して持たせたい情報があります。

共通要素内容
speed戦士の移動速度
setSpeed()速度を設定する処理
show()自分の情報を表示する処理

しかし、show() の中身は、戦士の種類によって変わります。

通常のサイヤ人戦士なら、名前や流派を表示すればよいかもしれません。

スーパーサイヤ人戦士なら、変身段階も表示したくなります。

神流の覚醒戦士なら、神の気や覚醒段階も表示したくなるかもしれません。

このように、共通部分は親クラスにまとめたいけれど、具体的な処理は子クラスごとに変えたい場合があります。

そこで使うのが抽象クラスです。

抽象クラスは、普通のクラスと同じようにフィールドや通常メソッドを持てます。

ただし、普通のクラスと大きく違う点があります。

それは、抽象クラスそのものからはオブジェクトを作成できないという点です。

抽象クラスは、完成品ではありません。

サブクラスのために共通部分をまとめた、未完成の土台です。

ドラゴンボール風にたとえると、抽象クラスは「戦士ならこういう共通ルールを持つはずだ」という共通訓練書のようなものです。

速度を持つ。
速度を設定できる。
自分の情報を表示できる。

ここまでは共通ルールとして決めておきます。

しかし、実際にどんな自己紹介をするかは、戦士ごとに違います。

だから、親クラスでは show() の形だけを決めて、具体的な中身は子クラスに任せます。

この記事では、具体的なプログラムとして Sample1.java を例示しながら、抽象クラス、抽象メソッド、共通メソッド、サブクラスでの実装、抽象クラス型配列、多態性の流れを、ドラゴンボールの世界観で解説します。

抽象クラスとは何か

抽象クラスは、クラスの前に abstract を付けて宣言する特別なクラスです。

たとえば、サイヤ人戦士の共通土台を表す Warrior クラスを抽象クラスにするなら、次のように書きます。

abstract class Warrior
{
}

このように abstract class と書くことで、そのクラスは抽象クラスになります。

抽象クラスは、次のような役割を持ちます。

要素抽象クラスでできること
フィールド共通して持たせたい値を定義できる
通常のメソッド共通して使える処理を定義できる
抽象メソッド子クラスで必ず作らせたい処理を宣言できる
オブジェクト作成抽象クラス自身からは作成できない

ここで大切なのは、抽象クラスは「全部が未完成」という意味ではないことです。

共通して使える部分は完成させておけます。

一方で、子クラスごとに違う部分は、抽象メソッドとして「ここは子クラスで決めてください」と残しておけます。

ドラゴンボール風に言うと、Warrior 抽象クラスは「戦士の共通訓練書」です。

移動速度の考え方や、速度を設定する方法は全員共通です。

しかし、どんな戦士として自己紹介するかは、スーパーサイヤ人戦士、神流の覚醒戦士、支援型戦士などで変わります。

そのため、自己紹介メソッド show() は、親クラスでは中身を決めず、子クラスに完成を任せます。

抽象クラスはオブジェクトを作成できない

抽象クラスの大きな特徴は、new を使って直接オブジェクトを作成できないことです。

たとえば、次のような抽象クラスがあるとします。

abstract class Warrior
{
}

この場合、次のようには書けません。

// これはできない
Warrior warrior1 = new Warrior();

抽象クラスは未完成の土台なので、そのまま実体として作ることはできません。

ドラゴンボール風にたとえると、Warrior は「戦士という共通概念」です。

しかし、ただの「戦士という概念」だけでは、実際に戦場へ出すことはできません。

実際に登場させるには、具体的な戦士クラスが必要です。

抽象的な土台具体的なサブクラス
WarriorSuperSaiyanWarrior
WarriorGodSaiyanWarrior
WarriorSupportWarrior

Warrior 抽象クラスは、共通ルールを決めるための土台です。

しかし、それ自体は完成した戦士ではありません。

実際にオブジェクトを作るには、Warrior を継承した具体的なサブクラスを使います。

図:抽象クラスは共通訓練書のような土台

この図が示していること

この図では、Warrior 抽象クラスが、複数の戦士クラスに共通する土台になっていることを表しています。

Warrior には、speed、setSpeed()、abstract show() があります。

speed と setSpeed() は、すべての戦士が共通して使う部分です。

一方、show() は abstract show() として未完成のまま残されています。

そのため、Warrior から直接オブジェクトを作ることはできません。

下の SuperSaiyanWarrior、GodSaiyanWarrior、SupportWarrior は、Warrior を継承した具体的なサブクラスです。

それぞれが show() を自分のクラスに合うように完成させます。

この図から分かることは、抽象クラスは共通ルールをまとめ、具体的な処理はサブクラスに任せるための土台だということです。

抽象メソッドとは何か

抽象クラスで特に大切なのが、抽象メソッドです。

抽象メソッドとは、処理内容を書かずに、メソッドの形だけを宣言するメソッドです。

たとえば、次のように書きます。

abstract void show();

このように、メソッドにも abstract を付けます。

そして、メソッドの中身を表す { } は書きません。

これは、次のような意味です。

このメソッドは必ず必要です。
ただし、具体的な中身はサブクラスで決めてください。

ドラゴンボール風にたとえると、親クラスである Warrior は、すべての戦士に次のような約束を求めています。

親クラスの約束意味
戦士なら show() を持つこと自分の情報を表示できること
ただし内容は各戦士に任せる変身戦士、神流戦士、支援戦士で表示内容が違うから

たとえば、SuperSaiyanWarrior なら、名前、流派、変身段階を表示するかもしれません。

GodSaiyanWarrior なら、名前、流派、神の気の段階を表示するかもしれません。

SupportWarrior なら、名前、流派、支援能力を表示するかもしれません。

このように、抽象メソッドは「共通ルールとして必要だけれど、親クラスではまだ具体的に決めない処理」です。

抽象クラスの基本の形

抽象クラスは、普通のメンバと抽象メソッドを組み合わせて作れます。

たとえば、次のような形です。

abstract class Warrior
{
    protected int speed;

    public void setSpeed(int s)
    {
        speed = s;
    }

    abstract void show();
}

この例では、speed と setSpeed() は具体的に定義されています。

一方、show() は抽象メソッドです。

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

メンバ完成しているか役割
speed完成しているすべての戦士が持つ速度
setSpeed()完成している速度を設定する共通処理
show()未完成子クラスごとに表示内容を決める

つまり抽象クラスは、共通部分は完成させ、個別部分は子クラスに任せるクラスです。

ここがとても重要です。

抽象クラスは、ただ未完成なだけではありません。

共通部分を親クラスにまとめることで、子クラス側の重複を減らします。

さらに、抽象メソッドによって、子クラスが必ず持つべき処理を強制できます。

抽象クラスは何のために必要なのか

抽象クラスが必要になる理由は、共通ルールをまとめながら、子クラスごとの違いもきれいに表現したいからです。

たとえば、ドラゴンボール風に Warrior という抽象クラスを考えます。

戦士なら、共通して speed を持ち、速度を設定する setSpeed() を使えるとします。

しかし、show() で表示したい内容はクラスによって違います。

クラスshow() で表示したい内容
SuperSaiyanWarrior名前、流派、変身段階、速度
GodSaiyanWarrior名前、神の気、覚醒段階、速度
SupportWarrior名前、支援能力、速度

このとき、親クラスにすべての show() を無理に書くと、子クラスごとの違いを表現しにくくなります。

そこで、親クラスでは show() を抽象メソッドにしておきます。

abstract void show();

こうしておけば、サブクラスは必ず show() を定義する必要があります。

つまり、抽象クラスを使うと次のような設計ができます。

親クラスにまとめるもの子クラスに任せるもの
共通フィールドクラスごとの独自フィールド
共通メソッドクラスごとの表示内容
必ず持つべきメソッド名実際の処理内容

ドラゴンボール風に言うと、Warrior 抽象クラスは「戦士なら必ず自己紹介できること」というルールを決めます。

ただし、自己紹介の内容は、スーパーサイヤ人戦士、神流戦士、支援型戦士で変わります。

その違いは、それぞれのサブクラスに任せるのです。

サブクラスは抽象メソッドを定義しなければならない

抽象クラスを継承したサブクラスでオブジェクトを作成できるようにするには、抽象メソッドを具体的に定義する必要があります。

つまり、抽象メソッドはサブクラスでオーバーライドして完成させます。

たとえば、親クラスに次の抽象メソッドがあるとします。

abstract void show();

この親クラスを継承した SuperSaiyanWarrior では、show() の中身を定義します。

public void show()
{
    System.out.println("スーパーサイヤ人戦士の情報を表示します。");
}

もしサブクラスが抽象メソッドを定義しなければ、そのサブクラスも未完成のままです。

そのため、通常の完成したクラスとしてオブジェクトを作れません。

ドラゴンボール風にたとえると、Warrior は「戦士なら自己紹介の技 show() を必ず持ちなさい」と約束だけを決めています。

実際に戦場へ出る SuperSaiyanWarrior や SupportWarrior は、その show() の中身を自分で完成させなければなりません。

抽象メソッドは、サブクラスに責任を渡す仕組みです。

親クラスは「必要な技名」を決めます。
子クラスは「その技の中身」を完成させます。

図:抽象メソッドをサブクラスで完成させる

この図が示していること

この図では、Warrior 抽象クラスにある abstract void show(); を、サブクラスで完成させる流れを表しています。

Warrior では、show() の名前だけを決めています。

しかし、処理内容は書いていません。

そのため、show() は未完成の抽象メソッドです。

SuperSaiyanWarrior と SupportWarrior は、Warrior を継承した具体的なサブクラスです。

それぞれが show() を自分の内容に合わせて定義しています。

SuperSaiyanWarrior は、名前、流派、変身段階、速度を表示します。

SupportWarrior は、名前、流派、支援能力、速度を表示します。

この図から分かることは、抽象クラスは共通ルールを決め、サブクラスが具体的な処理を完成させるという役割分担です。

抽象クラスを定義してみる

ファイル名:Sample1.java

abstract class Warrior
{
    protected int speed;

    public void setSpeed(int s)
    {
        speed = s;
        System.out.println("速度を" + speed + "にしました。");
    }

    abstract void show();
}

class SuperSaiyanWarrior extends Warrior
{
    private String name;
    private String styleName;
    private int transformationLevel;

    public SuperSaiyanWarrior(String n, String s, int level)
    {
        name = n;
        styleName = s;
        transformationLevel = level;
        System.out.println(name + " 変身段階" + transformationLevel + "のスーパーサイヤ人戦士を作成しました。");
    }

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

class SupportWarrior extends Warrior
{
    private String name;
    private String styleName;
    private String supportSkill;

    public SupportWarrior(String n, String s, String skill)
    {
        name = n;
        styleName = s;
        supportSkill = skill;
        System.out.println(name + " 支援能力「" + supportSkill + "」の支援戦士を作成しました。");
    }

    public void show()
    {
        System.out.println("支援戦士の名前は" + name + "です。");
        System.out.println("流派は" + styleName + "です。");
        System.out.println("支援能力は" + supportSkill + "です。");
        System.out.println("速度は" + speed + "です。");
    }
}

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

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

        warriors[1] = new SupportWarrior("悟飯", "神流", "気の感知と仲間支援");
        warriors[1].setSpeed(50);

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

実行結果

悟空 変身段階1のスーパーサイヤ人戦士を作成しました。
速度を60にしました。
悟飯 支援能力「気の感知と仲間支援」の支援戦士を作成しました。
速度を50にしました。
スーパーサイヤ人戦士の名前は悟空です。
流派は亀仙流です。
変身段階は1です。
速度は60です。
支援戦士の名前は悟飯です。
流派は神流です。
支援能力は気の感知と仲間支援です。
速度は50です。

親クラス Warrior が抽象クラスになっている

このプログラムで中心になるのは、Warrior クラスが抽象クラスになっているところです。

abstract class Warrior

Warrior クラスには、共通フィールドとして speed があります。

protected int speed;

また、速度を設定する setSpeed() も持っています。

public void setSpeed(int s)
{
    speed = s;
    System.out.println("速度を" + speed + "にしました。");
}

この speed と setSpeed() は、SuperSaiyanWarrior でも SupportWarrior でも共通して使えます。

一方で、show() は抽象メソッドです。

abstract void show();

つまり Warrior クラスは、次のような設計です。

要素内容
speed全戦士が持つ共通フィールド
setSpeed()全戦士が使える共通メソッド
show()子クラスで必ず定義する抽象メソッド

ドラゴンボール風にたとえると、Warrior クラスは「戦士なら速度を持ち、速度を設定できる。そして必ず自己紹介できる」という共通ルールを決めています。

ただし、自己紹介の中身は戦士ごとに違うので、show() は子クラスに任せています。

SuperSaiyanWarrior と SupportWarrior が show() を完成させている

SuperSaiyanWarrior と SupportWarrior は、どちらも Warrior を継承したサブクラスです。

class SuperSaiyanWarrior extends Warrior

class SupportWarrior extends Warrior

そして、どちらも show() を自分のクラスに合うように定義しています。

クラスshow() で表示する内容
SuperSaiyanWarrior名前、流派、変身段階、速度
SupportWarrior名前、流派、支援能力、速度

SuperSaiyanWarrior の show() では、スーパーサイヤ人戦士らしい情報を表示しています。

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

SupportWarrior の show() では、支援戦士らしい情報を表示しています。

public void show()
{
    System.out.println("支援戦士の名前は" + name + "です。");
    System.out.println("流派は" + styleName + "です。");
    System.out.println("支援能力は" + supportSkill + "です。");
    System.out.println("速度は" + speed + "です。");
}

親クラス Warrior は show() という名前だけを決めています。

実際の表示内容は、それぞれのサブクラスが完成させています。

これが、抽象クラスとオーバーライドを組み合わせた設計です。

抽象クラスの配列でまとめて扱える

このプログラムでは、Warrior[] という配列を使っています。

Warrior[] warriors;
warriors = new Warrior[2];

Warrior は抽象クラスなので、次のように直接オブジェクトを作ることはできません。

// これはできない
Warrior warrior = new Warrior();

しかし、Warrior 型の変数や配列を用意することはできます。

そして、その配列には Warrior を継承した具体的なサブクラスのオブジェクトを入れられます。

warriors[0] = new SuperSaiyanWarrior("悟空", "亀仙流", 1);
warriors[1] = new SupportWarrior("悟飯", "神流", "気の感知と仲間支援");

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

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

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

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

呼び出し方は同じです。

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

実体実行される show()
SuperSaiyanWarriorSuperSaiyanWarrior の show()
SupportWarriorSupportWarrior の show()

このように、抽象クラスを使うと、共通の型でまとめながら、実体ごとの個性を活かせます。

ドラゴンボール風に言うと、戦士管理本部は全員を Warrior 型として名簿に並べます。

でも、実際に show() を命じると、スーパーサイヤ人戦士は変身段階を含めて自己紹介し、支援戦士は支援能力を含めて自己紹介します。

抽象クラスを使うと、なぜ分かりやすいコードになるのか

抽象クラスを使うと、コードの設計が分かりやすくなります。

理由は大きく2つあります。

1つ目は、共通ルールが見えることです。

Warrior クラスを見るだけで、次のことが分かります。

Warrior を見て分かること内容
speed を持つ戦士には速度がある
setSpeed() を使える速度を設定できる
show() を必ず持つ自分の情報を表示できる

2つ目は、まとめて扱いやすくなることです。

SuperSaiyanWarrior と SupportWarrior は違うクラスです。

しかし、どちらも Warrior のサブクラスなので、Warrior[] に入れてまとめて扱えます。

さらに、Warrior 側で show() を抽象メソッドとして宣言しているため、呼び出す側は安心して show() を呼べます。

ドラゴンボール風にたとえると、戦士管理本部は戦士の種類を細かく知らなくても、各自、情報を示せと同じ命令を出せます。

スーパーサイヤ人戦士はスーパーサイヤ人戦士らしく、支援戦士は支援戦士らしく、自分の情報を表示します。

抽象クラスと普通のクラスの違い

普通のクラスと抽象クラスの違いを整理すると、次のようになります。

比較項目普通のクラス抽象クラス
オブジェクト作成できる直接はできない
抽象メソッド持てない持てる
共通フィールド持てる持てる
通常メソッド持てる持てる
主な役割完成したクラスサブクラスの共通土台

普通のクラスは、そのまま実体化できる完成品です。

抽象クラスは、サブクラスに共通ルールを渡すための未完成の設計図です。

ドラゴンボール風にたとえると、普通のクラスは実際に戦場へ出られる具体的な戦士です。

抽象クラスは、戦士たちの共通規則をまとめた訓練書のようなものです。

図:抽象クラス型の配列でまとめて扱う

この図が示していること

この図では、抽象クラス Warrior 型の配列に、SuperSaiyanWarrior と SupportWarrior のオブジェクトを入れている様子を表しています。

Warrior は抽象クラスなので、new Warrior() のように直接オブジェクトは作れません。

しかし、Warrior 型の配列や変数として使うことはできます。

配列の中には、Warrior を継承した具体的なクラスのオブジェクトを入れられます。

配列要素実体
warriors[0]SuperSaiyanWarrior
warriors[1]SupportWarrior

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

warriors[i].show();

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

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

この図から分かることは、抽象クラスを使うと、共通の型でまとめながら、サブクラスごとの個性ある処理を動かせるということです。

ドラゴンボール風に抽象クラスを整理する

抽象クラスは、ドラゴンボール風にたとえると、戦士ならこうあるべきという共通ルールをまとめた、未完成の設計図です。

Warrior クラスは、戦士の共通土台です。

Warrior が決めること内容
speed を持つすべての戦士に速度がある
setSpeed() を使える速度を設定できる
show() を持つ情報を表示できる必要がある

ただし、show() の中身は戦士ごとに違います。

サブクラスshow() の中身
SuperSaiyanWarrior名前、流派、変身段階、速度
SupportWarrior名前、流派、支援能力、速度

つまり抽象クラスは、共通ルールを決めながら、個性の部分はサブクラスに任せる仕組みです。

この考え方を使うと、次のような効果があります。

抽象クラスでできること効果
共通部分を親にまとめる重複を減らせる
必須メソッドを宣言するサブクラスに実装を強制できる
抽象クラス型でまとめる複数のサブクラスを同じ形で扱える
実体ごとに処理を変える多態性を活かせる

抽象クラスは、未完成だから弱いのではありません。

共通部分をまとめ、子クラスに必ず作ってほしい処理を示し、サブクラスを同じ型で扱えるようにする。

その意味で、抽象クラスは、未完成だからこそ共通の土台として強いクラスです。

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

ポイント内容
抽象クラスabstract を付けて宣言する未完成のクラス
オブジェクト作成抽象クラス自身からは直接作れない
共通フィールド抽象クラスに定義できる
通常メソッド抽象クラスに定義できる
抽象メソッド処理内容を書かず、サブクラスに実装を任せる
サブクラスの役割抽象メソッドをオーバーライドして完成させる
配列での利用抽象クラス型の配列にサブクラスの実体を入れられる
多態性同じ show() 呼び出しでも実体に応じて動きが変わる

抽象クラスを理解すると、共通部分を親クラスにまとめながら、具体的な違いは子クラスに任せる設計ができるようになります。

Warrior 抽象クラスは、すべての戦士に共通するルールを決めます。

SuperSaiyanWarrior や SupportWarrior は、そのルールを受け継ぎ、自分らしい show() を完成させます。

この「共通ルールは親、具体的な個性は子」という考え方が、抽象クラスを使う大きな意味です。