抽象クラスと抽象メソッドを使いこなそう!

docs

前、継承について学んだよね。継承を使えば、すでにあるクラスの機能を受け継いで、新しいクラスを効率よく作れるんだったね。
継承の基本:クラスの再利用 | ToolDocs

今回は、その継承のさらに便利な使い方、**「抽象クラス」と「抽象メソッド」**について掘り下げていこう!これらを使いこなすと、もっと柔軟で堅牢なプログラムが書けるようになるんだ。


抽象クラスって何?

抽象クラスとは、その名の通り「抽象的な」クラスのこと。ざっくり言うと、**「不完全なクラス」**なんだ。どういうことかって?

例えば、動物を表現するクラスを考えてみよう。Animalクラスを作って、その中にwalk()(歩く)メソッドを定義するとするよね。でも、動物って一口に言っても、犬もいれば猫もいる、鳥もいるよね。それぞれの動物によって「歩き方」って全然違うはずだ。犬は4本足で歩くし、鳥は2本足で歩く(または飛ばない)かもしれない。

Animalクラスのwalk()メソッドに、具体的な「歩き方」を定義するのは難しいよね。だって、どんな動物でも共通の「歩き方」なんてないからね。

そんな時に役立つのが抽象クラスなんだ。Animalクラスを抽象クラスにすることで、**「これはあくまで動物という漠然とした概念を表すクラスだよ。具体的な動物は、このクラスを継承して作ってね!」**という意思表示ができるんだ。

抽象クラスはabstractキーワードを使って定義するよ。

Java

public abstract class Animal {
    // 抽象クラスなので、インスタンスは直接作れないよ
}

これでAnimalクラスを抽象クラスにできたね。抽象クラスは、それ単体ではオブジェクト(インスタンス)を作ることができないんだ。なぜなら、不完全だからね。具体的な動きは、このAnimalクラスを継承した子クラスで定義して初めて使えるようになるんだ。


抽象メソッドって何?

じゃあ、さっきのwalk()メソッドはどうするの?具体的な処理が書けないなら、メソッド自体いらないの?

いやいや、いるんだな、これが!ここで登場するのが抽象メソッドだよ。

抽象メソッドも、抽象クラスと同じくabstractキーワードを使って定義するんだけど、一番の特徴は、メソッドの本体(中身の処理)を持たないことなんだ。つまり、{}(波括弧)の中身が空っぽなんだね。

こんな感じ。

Java

public abstract class Animal {
    // 抽象メソッド
    public abstract void walk(); // walkメソッドは、子クラスで必ず実装してね!
}

このwalk()メソッドは、「動物にはwalk()という振る舞いがあるはずだけど、具体的なやり方はまだ決まってないよ。このAnimalクラスを継承する子クラスは、必ずこのwalk()メソッドを自分のやり方で実装しなさい!」というルールを定めているんだ。

これを**「実装を強制する」**って言うんだけど、これこそが抽象クラスと抽象メソッドの強力な点なんだ。

例えば、Animalクラスを継承してDogクラスとCatクラスを作るとするよね。

Java

// Dogクラス
public class Dog extends Animal {
    @Override // アノテーションについては後で詳しく説明するね!
    public void walk() {
        System.out.println("犬はトコトコ歩きます。");
    }
}

// Catクラス
public class Cat extends Animal {
    @Override
    public void walk() {
        System.out.println("猫はしなやかに歩きます。");
    }
}

見ての通り、DogクラスもCatクラスも、それぞれwalk()メソッドをオーバーライドして、自分たちの「歩き方」を具体的に定義しているよね。もし、Dogクラスがwalk()メソッドを実装し忘れたら、Javaがエラーを出して教えてくれるんだ。

これで、「すべての動物は歩く」という共通の振る舞いを定義しつつ、それぞれの動物がどのように歩くかは子クラスに任せる、ということが実現できるんだね!


抽象クラスと抽象メソッドの使いどころ

  • 共通の振る舞いを定義し、子クラスに実装を強制したい場合: さっきのAnimalクラスとwalk()メソッドの例がまさにこれだね。
  • テンプレートメソッドパターン: ある処理の流れは決まっているけど、途中の具体的なステップは子クラスで自由に決めさせたい、なんて場合にも使えるよ。

例えば、ゲームのキャラクタークラスを考えてみよう。

Java

// プレイヤーキャラクターの抽象クラス
public abstract class PlayerCharacter {
    protected String name;
    protected int hp;

    public PlayerCharacter(String name, int hp) {
        this.name = name;
        this.hp = hp;
    }

    // 攻撃方法(抽象メソッド)
    public abstract void attack();

    // ダメージを受ける処理(共通の振る舞い)
    public void takeDamage(int damage) {
        this.hp -= damage;
        System.out.println(name + "は" + damage + "のダメージを受けた!残りHP:" + hp);
    }

    // レベルアップ時の処理(抽象メソッド)
    public abstract void levelUp();
}

// 剣士クラス
public class Swordsman extends PlayerCharacter {
    public Swordsman(String name) {
        super(name, 100); // 初期HPは100
    }

    @Override
    public void attack() {
        System.out.println(name + "は剣で斬りつけた!");
    }

    @Override
    public void levelUp() {
        this.hp += 20;
        System.out.println(name + "はレベルアップした!HPが20増えて" + hp + "になった!");
    }
}

// 魔法使いクラス
public class Magician extends PlayerCharacter {
    public Magician(String name) {
        super(name, 80); // 初期HPは80
    }

    @Override
    public void attack() {
        System.out.println(name + "は魔法を唱えた!");
    }

    @Override
    public void levelUp() {
        this.hp += 10;
        System.out.println(name + "はレベルアップした!HPが10増えて" + hp + "になった!");
    }
}

この例では、PlayerCharacterクラスは抽象クラスで、attack()levelUp()が抽象メソッドだね。

  • すべてのプレイヤーキャラクターはattack()(攻撃)とlevelUp()(レベルアップ)の振る舞いがあるはずだけど、その具体的な方法はクラスによって違う。
  • でも、takeDamage()(ダメージを受ける)はどんなキャラクターでも共通の処理だから、PlayerCharacterクラスで具体的に実装している。

これで、Swordsman(剣士)やMagician(魔法使い)といった具体的なキャラクタークラスを作る際に、attack()levelUp()の実装を強制できるから、実装漏れを防げるし、統一感のある設計ができるんだ。

実際に使ってみよう!

Java

public class Game {
    public static void main(String[] args) {
        // PlayerCharacter pc = new PlayerCharacter("名無し", 50); // エラー!抽象クラスはインスタンス化できない

        Swordsman arthur = new Swordsman("アーサー");
        Magician merlin = new Magician("マーリン");

        System.out.println("--- 戦闘開始! ---");
        arthur.attack();
        merlin.attack();

        arthur.takeDamage(30);
        merlin.takeDamage(20);

        System.out.println("--- レベルアップ! ---");
        arthur.levelUp();
        merlin.levelUp();
    }
}

実行結果はこんな感じになるよ。

--- 戦闘開始! ---
アーサーは剣で斬りつけた!
マーリンは魔法を唱えた!
アーサーは30のダメージを受けた!残りHP:70
マーリンは20のダメージを受けた!残りHP:60
--- レベルアップ! ---
アーサーはレベルアップした!HPが20増えて90になった!
マーリンはレベルアップした!HPが10増えて70になった!

完璧だね!

paizaで実行した結果


@Overrideアノテーションについて

さっきから出てきた@Overrideってなんだろう?これはアノテーションって呼ばれるものの一つで、特別な意味を持つ目印みたいなものなんだ。

@Overrideは、「このメソッドは親クラス(またはインターフェース)のメソッドをオーバーライドしていますよ」とJavaコンパイラに教えてあげる役割があるんだ。

もし@Overrideを付けていて、実際にはオーバーライドになっていない(例えば、メソッド名が間違っているとか、引数が違うとか)場合は、コンパイラがエラーを出してくれるんだ。これによって、タイプミスなんかによるバグを防ぐことができるから、積極的に使うようにしようね!


まとめ

  • 抽象クラスabstractキーワードで定義する不完全なクラス。直接インスタンス化はできない。
  • 抽象メソッドabstractキーワードで定義し、メソッドの本体を持たないメソッド。抽象クラスの中にしか定義できない。
  • 抽象クラスを継承する具象クラス(抽象ではない普通のクラス)は、すべての抽象メソッドを必ず実装(オーバーライド)しなければならない
  • これらを使うことで、共通の振る舞いを定義しつつ、具体的な実装は子クラスに強制するという、柔軟でバグの少ない設計ができるようになるよ!

抽象クラスと抽象メソッドは、Javaでオブジェクト指向プログラミングをする上でとても重要な概念だよ。最初は少し難しく感じるかもしれないけど、慣れてくるとプログラムの設計がしやすくなるのが実感できるはず!

次はインターフェースについて学んでいこう!インターフェースも抽象クラスと似ているようで違う、これまた強力なツールなんだ。お楽しみに!


関連リンク

コメント

タイトルとURLをコピーしました