Java道|抽象クラスのしくみ

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

12章では、Javaのオブジェクト指向の中でも少し特別なしくみである 抽象クラスインターフェイス を学んでいきます。

その最初のテーマが、抽象クラスです。

抽象クラスは、普通のクラスと同じようにフィールドやメソッドを持てます。
しかし、普通のクラスと大きく違う点があります。

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

なぜなら、抽象クラスは「完成品」ではなく、サブクラスのための共通の土台だからです。

鬼滅の刃風にたとえると、抽象クラスは「和風剣士ならこういう共通ルールを持つはずだ」という設計図です。

たとえば、剣士なら次のような共通要素を持っていそうです。

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

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

一般隊士なら名前や階級を表示するかもしれません。
柱なら担当区域や呼吸の型も表示したいかもしれません。
医療担当の剣士なら、特技や支援能力を表示したいかもしれません。

このように、共通部分は親クラスにまとめたいけれど、具体的な処理は子クラスごとに変えたい。
そんなときに使うのが抽象クラスです。

抽象クラスとは何か

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

abstract class Slayer
{
}

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

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

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

つまり抽象クラスは、全部が未完成というわけではありません。

共通して使える部分は完成させておきます。
一方で、子クラスごとに違う部分は、抽象メソッドとして「ここは子クラスで決めてください」と残しておきます。

鬼滅の刃風にたとえると、抽象クラスは「剣士の共通訓練書」のようなものです。
速度の設定方法など、全員共通の内容は書かれています。
でも、実際にどんな自己紹介をするか、どんな能力を見せるかは、各剣士クラスに任せるイメージです。

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

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

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

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

abstract class Slayer
{
}

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

// これはできない
Slayer slayer1 = new Slayer();

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

鬼滅の刃風にたとえると、Slayer は「剣士という概念」です。
しかし、ただの「剣士」という概念だけでは、具体的な人物として任務に出せません。

実際に登場させるには、次のような具体的なクラスが必要です。

抽象的な土台具体的なサブクラス
SlayerWaterSlayer
SlayerFlameSlayer
SlayerMedicalSlayer

つまり、抽象クラスはサブクラスを作るための土台にはなります。
しかし、それ自体は完成品ではないため、直接オブジェクト化できません。

抽象メソッドとは何か

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

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

abstract void show();

このように、メソッドにも abstract を付けます。
そして、メソッドの中身を表す { } は書きません。

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

鬼滅の刃風にたとえると、親クラスである Slayer は、次のように命じています。

親クラスの約束意味
剣士なら show() を持つこと自分の情報を表示できること
ただし内容は各剣士に任せる水柱、炎柱、医療担当で表示内容が違うから

たとえば、WaterSlayer なら水の呼吸や担当区域を表示するかもしれません。
MedicalSlayer なら治療技術や支援能力を表示するかもしれません。

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

抽象クラスの基本の形

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

abstract class Slayer
{
    protected int speed;

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

    abstract void show();
}

この例では、speed と setSpeed() は具体的に定義されています。
一方、show() は抽象メソッドです。

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

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

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

以下の条件でイラストを作成してください。

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

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

たとえば、鬼滅の刃風に Slayer という抽象クラスを考えます。

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

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

クラスshow() で表示したい内容
WaterSlayer名前、呼吸の型、速度
FlameSlayer名前、火力、速度
MedicalSlayer名前、治療技術、速度

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

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

abstract void show();

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

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

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

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

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

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

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

abstract void show();

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

public void show()
{
    System.out.println("水の剣士の情報を表示します。");
}

もしサブクラスが抽象メソッドを定義しなければ、そのサブクラスも未完成のままです。
そのため、通常の完成したクラスとしてオブジェクトを作れません。

鬼滅の刃風にたとえると、親クラス Slayer は「剣士なら自己紹介の技 show() を必ず持ちなさい」と約束だけを決めています。
実際に任務に出る WaterSlayer や MedicalSlayer は、その show() の中身を自分で完成させなければなりません。

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

ファイル名:Sample1.java

abstract class Slayer
{
    protected int speed;

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

    abstract void show();
}

class WaterSlayer extends Slayer
{
    private String name;
    private String breathingStyle;

    public WaterSlayer(String n, String bs)
    {
        name = n;
        breathingStyle = bs;
        System.out.println(name + " 水の呼吸「" + breathingStyle + "」の剣士を作成しました。");
    }

    public void show()
    {
        System.out.println("水の剣士の名前は" + name + "です。");
        System.out.println("呼吸の型は" + breathingStyle + "です。");
        System.out.println("速度は" + speed + "です。");
    }
}

class MedicalSlayer extends Slayer
{
    private String name;
    private String healingSkill;

    public MedicalSlayer(String n, String hs)
    {
        name = n;
        healingSkill = hs;
        System.out.println(name + " 治療技術「" + healingSkill + "」の支援剣士を作成しました。");
    }

    public void show()
    {
        System.out.println("支援剣士の名前は" + name + "です。");
        System.out.println("治療技術は" + healingSkill + "です。");
        System.out.println("速度は" + speed + "です。");
    }
}

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

        slayers[0] = new WaterSlayer("水月", "水流斬り");
        slayers[0].setSpeed(60);

        slayers[1] = new MedicalSlayer("胡蝶香", "藤花治療");
        slayers[1].setSpeed(50);

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

実行結果

水月 水の呼吸「水流斬り」の剣士を作成しました。
速度を60にしました。
胡蝶香 治療技術「藤花治療」の支援剣士を作成しました。
速度を50にしました。
水の剣士の名前は水月です。
呼吸の型は水流斬りです。
速度は60です。
支援剣士の名前は胡蝶香です。
治療技術は藤花治療です。
速度は50です。

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

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

abstract class Slayer

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

protected int speed;

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

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

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

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

abstract void show();

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

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

鬼滅の刃風にたとえると、Slayer クラスは「剣士なら速度を持ち、速度を設定できる。そして必ず自己紹介できる」という共通ルールを決めています。

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

WaterSlayer と MedicalSlayer が show() を完成させている

WaterSlayer と MedicalSlayer は、どちらも Slayer を継承したサブクラスです。

class WaterSlayer extends Slayer

class MedicalSlayer extends Slayer

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

クラスshow() で表示する内容
WaterSlayer名前、呼吸の型、速度
MedicalSlayer名前、治療技術、速度

WaterSlayer の show() では、水の剣士らしい情報を表示しています。

public void show()
{
    System.out.println("水の剣士の名前は" + name + "です。");
    System.out.println("呼吸の型は" + breathingStyle + "です。");
    System.out.println("速度は" + speed + "です。");
}

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

public void show()
{
    System.out.println("支援剣士の名前は" + name + "です。");
    System.out.println("治療技術は" + healingSkill + "です。");
    System.out.println("速度は" + speed + "です。");
}

親クラス Slayer は show() という名前だけを決めています。
実際の表示内容は、それぞれのサブクラスが完成させています。

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

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

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

Slayer[] slayers;
slayers = new Slayer[2];

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

// これはできない
Slayer slayer = new Slayer();

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

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

slayers[0] = new WaterSlayer("水月", "水流斬り");
slayers[1] = new MedicalSlayer("胡蝶香", "藤花治療");

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

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

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

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

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

実体実行される show()
WaterSlayerWaterSlayer の show()
MedicalSlayerMedicalSlayer の show()

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

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

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

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

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

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

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

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

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

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

鬼滅の刃風にたとえると、司令部は隊士の種類を細かく知らなくても、
「各自、情報を示せ」
と同じ命令を出せます。

水の剣士は水の剣士らしく、支援剣士は支援剣士らしく、自分の情報を表示します。

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

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

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

普通のクラスは、そのまま実体化できる完成品です。
抽象クラスは、サブクラスに共通ルールを渡すための未完成の設計図です。

鬼滅の刃風にたとえると、普通のクラスは実際に任務へ出られる具体的な剣士です。
抽象クラスは、剣士たちの共通規則をまとめた訓練書のようなものです。

図:抽象クラスとサブクラスの役割分担

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

この図が示していること

この図では、抽象クラス Slayer と、そのサブクラスである WaterSlayer、MedicalSlayer の役割分担を表しています。

上部の Slayer は抽象クラスです。
共通フィールド speed と共通メソッド setSpeed() を持っています。

一方で、show() は abstract show() として未完成のままです。
そのため、Slayer から直接オブジェクトを作ることはできません。

下部の WaterSlayer と MedicalSlayer は、Slayer を継承した具体的なクラスです。
それぞれが show() を自分の内容に合わせて完成させています。

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

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

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

この図が示していること

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

Slayer は抽象クラスなので、new Slayer() のように直接オブジェクトは作れません。
しかし、Slayer 型の配列や変数として使うことはできます。

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

配列要素実体
slayers[0]WaterSlayer
slayers[1]MedicalSlayer

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

slayers[i].show();

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

実体動くメソッド
WaterSlayerWaterSlayer の show()
MedicalSlayerMedicalSlayer の show()

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

鬼滅の刃風に抽象クラスを整理する

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

Slayer クラスは、剣士の共通土台です。

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

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

サブクラスshow() の中身
WaterSlayer名前、呼吸の型、速度
MedicalSlayer名前、治療技術、速度

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

この考え方を使うと、

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

というように、継承と多態性をより整理して使えるようになります。

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

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

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

共通部分をまとめ、子クラスに必ず作ってほしい処理を示し、サブクラスを同じ型で扱えるようにする。
その意味で、抽象クラスは 未完成だからこそ、共通の土台として強いクラス です。