Java入門|抽象クラスのしくみと使い方(abstractの基礎)

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

12章では、少し特別なクラスとして、抽象クラスとインターフェイスを学んでいきます。
その最初のテーマになるのが、抽象クラスです。抽象クラスは、ふつうのクラスと同じようにフィールドやメソッドを持てますが、そのままでは完成していない、という特徴があります。だからこそ、共通の土台としてとても使いやすい存在になります。

ドラゴンボールでたとえると、抽象クラスは「戦士ならこういう共通ルールを持つはずだ」という設計図です。
ただし、その設計図はまだ未完成です。たとえば、戦士なら速度を持ち、戦士なら自分の情報を表示できるはずです。でも、その表示のしかたは戦士ごとに違います。悟空とベジータとピッコロで、見せるべき情報や表現はまったく同じではありません。そこで、共通の土台だけを親クラスにまとめ、具体的な処理は子クラスに任せる、という考え方が必要になります。

この役割を担うのが抽象クラスです。
抽象クラスを使うと、

  • 共通のフィールドや共通メソッドは親にまとめる
  • でも子クラスごとに必ず用意してほしい処理は、抽象メソッドとして宣言だけしておく
  • その結果、サブクラスをまとめて扱いやすくなる

という流れを作れます。

今回は、「抽象クラスのしくみと使い方」を、ドラゴンボールの戦士たちに置きかえながら、やわらかく丁寧に整理していきます。特に、

  • 抽象クラスとは何か
  • なぜオブジェクトを作成できないのか
  • 抽象メソッドとは何か
  • サブクラスでは何をしなければならないのか
  • 抽象クラスを使うと、なぜコードがわかりやすくなるのか

を順番に見ていきます。

抽象クラスとは何か

抽象クラスは、クラスの先頭に abstract を付けて宣言する特別なクラスです。
このクラスは、共通のフィールドや共通メソッドを持てますが、そのままではオブジェクトを作成できません。

つまり抽象クラスは、完成品のクラスというより、サブクラスのための共通の土台 と考えるとわかりやすいです。

ドラゴンボールでたとえるなら、抽象クラスは「戦士クラス」のような存在です。
戦士ならみんな共通して、

  • 速度を持つ
  • 情報を表示する機能を持つ

といったルールがあります。
でも、「どう表示するか」までは戦士ごとに違います。だから親クラスで全部を完成させるのではなく、共通部分だけをまとめた未完成の土台にしておくわけです。

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

抽象クラスの大きな特徴のひとつは、オブジェクトを作成できないことです。
つまり new を使って、その抽象クラス自身のオブジェクトを作ることはできません。

これはとても自然な話です。
なぜなら、抽象クラスは未完成だからです。未完成なままでは、具体的にどう振る舞うかが決まりきっていない部分があります。そんな状態のクラスから、そのまま実体を作ることはできません。

ドラゴンボールでいえば、「戦士」という概念そのものだけでは、まだ具体的なキャラクターになっていない状態です。
悟空でもベジータでもピッコロでもない、ただの「戦士の共通ルール」だけでは、そのまま登場人物として画面に出せないのと同じです。

つまり抽象クラスは、

  • サブクラスを作るための土台にはなる
  • でもそれ自体は完成品ではない

という立場のクラスです。

抽象メソッドとは何か

抽象クラスのもうひとつの大事な特徴が、抽象メソッドを持てることです。
抽象メソッドとは、処理内容を定義していないメソッドです。宣言だけがあり、中身は書かれていません。これにも abstract を付けます。

たとえば、戦士クラスに

abstract void show();

のようなメソッドがあるとします。
これは、「戦士なら show を必ず持つべきだ。でも内容は各戦士で決めてね」という意味です。

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

  • 戦士なら自分の情報を見せることはできるはず
  • でも何をどう見せるかは、戦士ごとに違う

という状態です。

だから抽象メソッドは、共通ルールとして必ず必要だが、親クラスではまだ具体的に決めないメソッド と考えると整理しやすいです。

抽象クラスの基本の形

抽象クラスは、だいたい次のような形になります。

要素内容
abstract class抽象クラスであることを表す
フィールド共通して持たせたい値を書く
通常のメソッド共通して使える処理を書く
抽象メソッド子クラスで必ず定義させたい処理を書く

つまり抽象クラスは、普通のクラスの機能と、未完成メソッドの宣言をあわせ持つクラスです。
全部が未完成というわけではなく、共通部分は完成済みで、子クラスに任せたい部分だけが未完成になっています。

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

ここで気になるのが、「なぜわざわざ未完成のクラスを作るのか」という点です。

答えは、共通ルールをまとめながら、子クラスごとに違う部分もきちんと表現したいから です。

たとえばドラゴンボールで、戦士クラスを考えるとします。

戦士なら共通して、

  • 速度を持つ
  • 速度を設定できる

といった機能があります。
でも、自分の情報を表示する show は、クラスごとに違うかもしれません。

  • サイヤ人なら、名前・戦闘力・速度を見せる
  • ナメック星人なら、名前・特殊能力・速度を見せる
  • 地球人戦士なら、名前・武器・速度を見せる

このとき、親クラスで共通部分だけをまとめて、show は抽象メソッドにしておけば、子クラスは必ず自分に合った show を持つことになります。
これが抽象クラスの大きな価値です。

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

抽象クラスを継承したサブクラスでオブジェクトを作成できるようにするためには、親クラスから受け継いだ抽象メソッドの内容を、サブクラスできちんと定義しなければなりません。
つまり、抽象メソッドは必ずオーバーライドして完成させる必要があります。

これはとても大事なルールです。

ドラゴンボールでいえば、親クラスの「戦士」は
「show という技を必ず持つべきだ」
と約束だけしています。
でも実際に戦士を登場させるには、

  • サイヤ人としての show
  • 飛行機にあたる別の型としての show

のように、具体的な処理を書かなければいけません。

もし子クラスがその定義をしなければ、その子クラスも未完成のままです。
だから、そのままではオブジェクトを作成できません。

サンプルプログラム:抽象クラスの使い方

ここでは、ドラゴンボール風のクラスで、抽象クラスの使い方を見ていきます。
「戦士クラス」とその派生クラスを用いて整理します。内容は、抽象クラス Warrior に共通の速度設定機能を持たせ、サブクラス Saiyan と NamekianWarrior がそれぞれ自分に合った show() を定義する流れです。

ファイル名:Sample1.java

abstract class Warrior
{
    protected int speed;

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

    abstract void show();
}

class Saiyan extends Warrior
{
    private String name;
    private int power;

    public Saiyan(String n, int p)
    {
        name = n;
        power = p;
        System.out.println(name + " 戦闘力" + power + "のサイヤ人を作成しました。");
    }

    public void show()
    {
        System.out.println("サイヤ人の名前は" + name + "です。");
        System.out.println("戦闘力は" + power + "です。");
        System.out.println("速度は" + speed + "です。");
    }
}

class NamekianWarrior extends Warrior
{
    private String name;
    private String specialSkill;

    public NamekianWarrior(String n, String sk)
    {
        name = n;
        specialSkill = sk;
        System.out.println(name + " 特技" + specialSkill + "のナメック星人戦士を作成しました。");
    }

    public void show()
    {
        System.out.println("ナメック星人戦士の名前は" + name + "です。");
        System.out.println("特技は" + specialSkill + "です。");
        System.out.println("速度は" + speed + "です。");
    }
}

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

        warriors[0] = new Saiyan("ベジータ", 18000);
        warriors[0].setSpeed(60);

        warriors[1] = new NamekianWarrior("ピッコロ", "魔貫光殺砲");
        warriors[1].setSpeed(500);

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

実行結果

ベジータ 戦闘力18000のサイヤ人を作成しました。
速度を60にしました。
ピッコロ 特技魔貫光殺砲のナメック星人戦士を作成しました。
速度を500にしました。
サイヤ人の名前はベジータです。
戦闘力は18000です。
速度は60です。
ナメック星人戦士の名前はピッコロです。
特技は魔貫光殺砲です。
速度は500です。

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

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

abstract class Warrior

ここでは、共通フィールドとして speed を持ち、共通メソッドとして setSpeed() を持っています。
でも show() は抽象メソッドです。

abstract void show();

つまり Warrior クラスは、

  • 速度という共通要素は持つ
  • 速度を設定する処理も持つ
  • でも show の中身は子クラスに任せる

という設計になっています。

Saiyan と NamekianWarrior は何をしているのか

Saiyan と NamekianWarrior は、どちらも Warrior を継承したサブクラスです。
そして両方とも、show() を自分のクラスに合うように定義しています。

クラスshow() で表示する内容
Saiyan名前、戦闘力、速度
NamekianWarrior名前、特技、速度

ここがとても大切です。
親クラスは show() という名前だけを決めています。
でも中身は子クラスに任せています。

だから、サイヤ人はサイヤ人らしい表示、ナメック星人戦士はナメック星人戦士らしい表示ができます。
この「共通の名前を守りながら、中身はクラスごとに違う」というのが、抽象クラスとオーバーライドの組み合わせの強さです。

抽象クラスの配列でまとめて扱えるのが大きな利点

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

Warrior[] warriors;

ここに、

  • Saiyan
  • NamekianWarrior

のオブジェクトを入れています。

これは、どちらも Warrior のサブクラスだからできることです。
そしてループの中では、ただ

warriors[i].show();

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

それでも、

  • サイヤ人にはサイヤ人の show
  • ナメック星人戦士にはナメック星人戦士の show

が動きます。

ここが抽象クラスのとても便利なところです。

親クラス側で「show というメソッドを必ず持つ」と決めているからこそ、呼び出す側は細かいクラスの違いを気にせず、同じ show() でまとめて扱えます。

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

抽象クラスを使うとわかりやすいコードを書けます。
その理由は、大きく分けると次の 2 つです。

共通ルールがひと目でわかる

抽象クラスを見ると、

  • この系統のクラスは何を共通で持つのか
  • サブクラスは何を必ず実装しなければいけないのか

がすぐにわかります。

今回なら、Warrior を見ただけで

  • 速度を持つ
  • setSpeed() を持つ
  • show() を必ず持つべき

とわかります。

まとめて扱いやすくなる

サブクラスごとに違う処理を持っていても、抽象クラスの型でまとめて扱えます。
しかも抽象メソッドは必ず子クラスで定義されるので、安心して共通名で呼び出せます。

ドラゴンボール風に言えば、

  • 戦士なら必ず show を持つ
  • だから戦士たちをまとめて並べても、同じ命令で動かせる

ということです。

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

ここで、普通のクラスと抽象クラスを整理しておくと理解が深まります。

比較項目普通のクラス抽象クラス
オブジェクト作成できるできない
抽象メソッド持てない持てる
役割完成したクラス共通ルールを持つ未完成の土台

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

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

最後に、ドラゴンボールの感覚でひとつにまとめてみましょう。

抽象クラスは、「戦士ならこうあるべき」という共通ルールをまとめた土台です。

  • 戦士なら速度を持つ
  • 戦士なら show を持つ
  • でも show の中身は戦士ごとに違う

だから親クラスでは show の宣言だけを置いておき、悟空型やベジータ型、ピッコロ型のような子クラス側で具体的な中身を決めます。
その結果、

  • 共通部分は親にまとまる
  • 個性は子に任せられる
  • しかも親クラス型でまとめて扱える

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

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

図で抽象クラスのしくみを整理する

抽象クラスは、親クラスと子クラスの役割分担を図で見るとかなりわかりやすくなります。

上部の Warriorクラス は抽象クラスなので、共通の speed と setSpeed() を持っていますが、show() はまだ未完成です。
そのため、直接オブジェクトを作ることはできません。

下の Saiyanクラス と NamekianWarriorクラス は、それぞれ show() の中身を定義した完成済みのクラスです。
そのため、実際にオブジェクトを作成できます。

さらに下部では、Warrior[] の配列で両方のオブジェクトをまとめて扱いながら、show() を同じ形で呼び出せることを表しています。
ここから、抽象クラスが共通ルールを整え、サブクラスをわかりやすくまとめる土台になっていることが見えてきます。