1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オブジェクト指向って何?RPGで理解する超入門 (継承の基礎)

1
Last updated at Posted at 2025-12-20

title_banner_v2.png

はじめに

プログラミング講師をしていると、「戦士クラスと魔法使いクラスで同じコードをコピペしてしまいました」という相談を頻繁に受けます。これは継承の概念を理解していないために起こる、初心者がよくやってしまうアンチパターンです。

本記事では、RPGゲームの「職業の派生システム」を例に、継承の基本から実践的な使い方まで徹底的に解説します。冒険者という基本職業から、戦士や魔法使いが派生し、さらに剣士・騎士・魔道士・聖職者へと進化するシステムを実装しながら学びましょう。

この記事で学べること

  • 継承(Inheritance) の概念と利点
  • 親クラスと子クラスの関係(基本となるクラスと、そこから機能を引き継ぐクラス)
  • extendsキーワードの使い方
  • メソッドのオーバーライド(親のメソッドを子クラス用に書き換える)
  • superキーワードの活用(親の力を借りる)
  • 継承の設計指針

対象読者

  • Javaの基本文法を学んだ方
  • クラスとコンストラクタを理解している方
  • コードの重複に悩んでいる方
  • オブジェクト指向の核心を理解したい方

所要時間

約20〜25分。実践的な内容が多いです。

このページのコードは少し長めです
すぐにコピペして動かすよりも、
なぜ「この形になっているのか」 を追いながら読んでみてください

AIがコードを書く時代だからこそプログラムを読む力=考え方を読む力
が大切になります

前回までのおさらいの記事一覧

第1回〜第6回をまだ読んでいない方は、先にそちらをご覧ください。

1. 継承とは何か?

1.1 継承の基本概念

継承(Inheritance) は、既存のクラスの機能を引き継いで、新しいクラスを作成する仕組みです。これにより、コードの重複を避け、保守性と拡張性を向上させることができます。

🏠 例えるなら:

  • 親の家の設計図を受け継いで、自分なりにアレンジした新しい家を建てるイメージ
  • 基本的な間取りや設備は親から引き継いで、自分だけの部屋を追加する

inheritance_concept_v2.png

継承の登場人物:

  • 親クラス(スーパークラス、基底クラス): 機能を提供する側。基本となるクラス
  • 子クラス(サブクラス、派生クラス): 機能を受け継ぐ側。親から機能を引き継ぐクラス

1.2 なぜ継承が必要なのか?

継承がない場合の問題:

/**
 * 戦士クラス(継承なし)
 */
public class Warrior {
    private String name;
    private int hp;
    private int level;
    private int power;
    
    public void attack() {
        System.out.println(name + "の攻撃!");
    }
    
    public void defend() {
        System.out.println(name + "は防御した!");
    }
    
    public void slash() {
        System.out.println(name + "の斬撃!");
    }
}

/**
 * 魔法使いクラス(継承なし)
 */
public class Mage {
    private String name;  // 重複!
    private int hp;       // 重複!
    private int level;    // 重複!
    private int magic;
    
    public void attack() {  // 重複!
        System.out.println(name + "の攻撃!");
    }
    
    public void defend() {  // 重複!
        System.out.println(name + "は防御した!");
    }
    
    public void cast() {
        System.out.println(name + "は魔法を唱えた!");
    }
}

問題点:

  • name, hp, levelが重複している
  • attack(), defend()が重複している
  • 修正時に全クラスを変更する必要がある
  • 新しい職業を追加するたびにコピペが増える

もっとわかりやすく:
同じレシピを何度もノートにコピーしているようなもの。修正したいときに全部書き換える必要があって大変!

1.3 継承を使った解決策

/**
 * 親クラス:冒険者
 * すべての職業の基礎となるクラス
 */
public class Adventurer {
    protected String name;
    protected int hp;
    protected int level;
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     */
    public Adventurer(String name, int hp) {
        this.name = name;
        this.hp = hp;
        this.level = 1;
    }
    
    /**
     * 攻撃する
     */
    public void attack() {
        System.out.println(name + "の攻撃!");
    }
    
    /**
     * 防御する
     */
    public void defend() {
        System.out.println(name + "は防御した!");
    }
    
    /**
     * 自己紹介
     */
    public void introduce() {
        System.out.println("私は" + name + "です!Lv." + level);
    }
    
    // getter/setter
    public String getName() { return name; }
    public int getHp() { return hp; }
    public void setHp(int hp) { this.hp = hp; }
    public int getLevel() { return level; }
    public void setLevel(int level) { this.level = level; }
}

/**
 * 子クラス:戦士
 * Adventurerを継承
 */
public class Warrior extends Adventurer {
    private int power;  // 戦士独自のフィールド
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     * @param power 攻撃力
     */
    public Warrior(String name, int hp, int power) {
        super(name, hp);  // 親のコンストラクタを呼ぶ
        this.power = power;
    }
    
    /**
     * 斬撃(戦士独自のメソッド)
     */
    public void slash() {
        System.out.println(name + "の斬撃!威力:" + power);
    }
    
    // getter/setter
    public int getPower() { return power; }
    public void setPower(int power) { this.power = power; }
}

/**
 * 子クラス:魔法使い
 * Adventurerを継承
 */
public class Mage extends Adventurer {
    private int magic;  // 魔法使い独自のフィールド
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     * @param magic 魔力
     */
    public Mage(String name, int hp, int magic) {
        super(name, hp);  // 親のコンストラクタを呼ぶ
        this.magic = magic;
    }
    
    /**
     * 魔法詠唱(魔法使い独自のメソッド)
     */
    public void cast() {
        System.out.println(name + "は魔法を唱えた!魔力:" + magic);
    }
    
    // getter/setter
    public int getMagic() { return magic; }
    public void setMagic(int magic) { this.magic = magic; }
}

改善された点:

  • 共通部分は親クラスに1箇所だけ
  • 各クラスは独自の機能だけを実装
  • 修正は親クラスだけでOK
  • 新しい職業を追加しやすい

もっとわかりやすく:
レシピ本の「基本レシピ」ページを1つ作って、それを参照するだけ。修正も1箇所で済むから楽ちん!

2. extendsキーワード:継承の実現

2.1 extendsの基本構文

public class 子クラス extends 親クラス {
    // 子クラス独自のフィールド・メソッド
}

わかりやすく言うと:

  • extendsは「〜から機能を引き継ぎます」という宣言
  • 「Warrior extends Adventurer」= 「戦士は冒険者から機能を引き継ぎます」

extends_keyword_v2.png

2.2 継承の仕組み

/**
 * 継承のデモンストレーション
 */
public class InheritanceDemo {
    public static void main(String[] args) {
        // 戦士を作成
        Warrior hero = new Warrior("勇者", 100, 20);
        
        // 親クラスから継承したメソッドが使える
        hero.introduce();  // Adventurerのメソッド
        hero.attack();     // Adventurerのメソッド
        hero.defend();     // Adventurerのメソッド
        
        // 子クラス独自のメソッドも使える
        hero.slash();      // Warriorのメソッド
        
        // 親クラスのフィールドにもアクセスできる(protectedだから)
        System.out.println("HP: " + hero.getHp());
        System.out.println("レベル: " + hero.getLevel());
        
        // 子クラス独自のフィールド
        System.out.println("攻撃力: " + hero.getPower());
    }
}

実行結果:

私は勇者です!Lv.1
勇者の攻撃!
勇者は防御した!
勇者の斬撃!威力:20
HP: 100
レベル: 1
攻撃力: 20

2.3 is-a関係

継承は「is-a関係」を表します。
「A は B の一種である」 と言える関係のことです。
Javaではextends(継承) を使って表します。

わかりやすく言うと:

  • 「戦士 冒険者である」(Warrior is a Adventurer)
  • 「犬 動物である」(Dog is a Animal)
Warrior warrior = new Warrior("勇者", 100, 20);

// Warrior IS-A Adventurer(戦士は冒険者である)
System.out.println(warrior instanceof Adventurer);  // 出力: true
System.out.println(warrior instanceof Warrior);     // 出力: true

is-a関係の例:

  • 戦士 冒険者である ✅
  • 剣士 戦士である ✅
  • 騎士 戦士である ✅
  • 白魔道士 魔法使いである ✅

2.4 protectedアクセス修飾子

継承で重要なのがprotectedアクセス修飾子です。

わかりやすく言うと:

  • private: 自分の部屋の中だけ(他のクラスからは見えない)
  • protected: 家族にも見せる(子クラスからもアクセスできる)
  • public: 外部にも公開(誰でもアクセスできる)
修飾子 同じクラス 同じパッケージ 子クラス(サブクラス) 他のクラス
public
protected ×
(なし) × ×
private × × ×
Adventurer.java
public class Adventurer {
    // protectedなら子クラスからアクセスできる
    protected String name;
    protected int hp;
    
    // privateなら子クラスからもアクセスできない
    private String secretCode;
}
Warrior.java
public class Warrior extends Adventurer {
    public void displayInfo() {
        System.out.println(name);  // OK: protectedだから
        System.out.println(hp);    // OK: protectedだから
        // System.out.println(secretCode);  // エラー: privateだから
    }
}

3. メソッドのオーバーライド

3.1 オーバーライドとは

オーバーライド(Override) は、親クラスのメソッドを子クラスで再定義することです。同じ名前のメソッドを、より適した実装に置き換えることができます。

わかりやすく言うと:

  • 親から受け継いだ「攻撃」メソッドを、自分専用の「強攻撃」にカスタマイズする
  • レシピをアレンジして、自分好みの味に変える感じ

method_override_v2.png

3.2 オーバーライドの実装

Adventurer.java
/**
 * 親クラス:冒険者
 */
public class Adventurer {
    protected String name;
    protected int hp;
    
    public Adventurer(String name, int hp) {
        this.name = name;
        this.hp = hp;
    }
    
    /**
     * 攻撃(基本攻撃)
     */
    public void attack() {
        System.out.println(name + "の攻撃!");
        System.out.println("10ダメージ!");
    }
}
Warrior.java
/**
 * 子クラス:戦士
 */
public class Warrior extends Adventurer {
    private int power;
    
    public Warrior(String name, int hp, int power) {
        super(name, hp);
        this.power = power;
    }
    
    /**
     * 攻撃(オーバーライド)
     * 戦士の強力な攻撃に変更
     */
    @Override
    public void attack() {
        System.out.println(name + "の強攻撃!");
        System.out.println((10 + power) + "ダメージ!");
        System.out.println("会心の一撃!");
    }
}

3.3 @Overrideアノテーション

@Overrideアノテーションは、このメソッドがオーバーライドであることを明示します。

わかりやすく言うと:

  • 「このメソッドは親のメソッドを書き換えたものですよ」という目印
  • スペルミスや引数の間違いがあると、コンパイル時にエラーで教えてくれる

@Overrideの利点:

  1. 可読性向上 - オーバーライドであることが一目で分かる
  2. ミス防止 - メソッド名や引数を間違えるとコンパイルエラー
  3. 安全性 - 親クラスのメソッドが削除されたら警告
Warrior.java
/**
 * 子クラス:戦士
 */
public class Warrior extends Adventurer {
    // ✅ 正しいオーバーライド
    @Override
    public void attack() {
        System.out.println("強攻撃!");
    }
    
    // ❌ メソッド名が違う → コンパイルエラー
    // @Override
    // public void atack() {  // スペルミス
    //     System.out.println("強攻撃!");
    // }
    
    // ❌ 引数が違う → コンパイルエラー
    // @Override
    // public void attack(int damage) {
    //     System.out.println("強攻撃!");
    // }
}

3.4 オーバーライドのルール

オーバーライドの条件:

  1. メソッド名が同じ
  2. 引数の型・数・順序が同じ
  3. 戻り値の型が同じ(または、親の戻り値型の子クラス)
  4. アクセス修飾子は同じか、より広い範囲

わかりやすく言うと:

  • 「同じ名前・同じ引数」でないと、オーバーライドとして認識されない
  • アクセス範囲は狭くできない(protectedをprivateにするのはNG)
Adventurer.java
/**
 * 親クラス:冒険者
 */
public class Adventurer {

    /**
     * 攻撃(基本攻撃)
     * protected, void, 引数なし
     */
    protected void attack() {
        System.out.println("攻撃!");
    }
}
Warrior.java
/**
 * 子クラス:戦士
 */
public class Warrior extends Adventurer {
    // ✅ OK: 同じprotected
    @Override
    protected void attack() {
        System.out.println("強攻撃!");
    }
    
    // ✅ OK: より広いpublic
    @Override
    public void attack() {
        System.out.println("強攻撃!");
    }
    
    // ❌ NG: より狭いprivate
    // @Override
    // private void attack() {
    //     System.out.println("強攻撃!");
    // }
}

4. superキーワード:親の力を借りる

4.1 superキーワードとは

superキーワードは、親クラスのメンバー(フィールド、メソッド、コンストラクタ)にアクセスするために使用します。

わかりやすく言うと:

  • superは「親クラスの〜を使います」という宣言
  • 親が作ってくれた料理に、自分の味付けを追加するイメージ

super_keyword_v2.png

4.2 super.メソッド() - 親のメソッドを呼ぶ

Adventurer.java
/**
 * 親クラス:冒険者
 */
public class Adventurer {
    protected String name;
    
    public Adventurer(String name) {
        this.name = name;
    }
    
    /**
     * 攻撃
     */
    public void attack() {
        System.out.println(name + "の攻撃!");
    }
}
Warrior.java
/**
 * 子クラス:戦士
 */
public class Warrior extends Adventurer {
    private int power;
    
    public Warrior(String name, int power) {
        super(name);
        this.power = power;
    }
    
    /**
     * 攻撃(親の攻撃 + 追加攻撃)
     */
    @Override
    public void attack() {
        // 親の攻撃を実行
        super.attack();
        
        // 追加の攻撃
        System.out.println("追加攻撃!威力:" + power);
    }
}

実行例:

SuperDemo.java
public class SuperDemo {
    public static void main(String[] args) {
        Warrior hero = new Warrior("勇者", 20);
        hero.attack();
    }
}

実行結果:

勇者の攻撃!
追加攻撃!威力:20

わかりやすく言うと:
親のメソッドをそのまま使いつつ、自分の機能を追加できる

4.3 super() - 親のコンストラクタを呼ぶ

Adventurer.java
/**
 * 親クラス:冒険者
 */
public class Adventurer {
    protected String name;
    protected int hp;
    protected int level;
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     */
    public Adventurer(String name, int hp) {
        this.name = name;
        this.hp = hp;
        this.level = 1;
        System.out.println("冒険者を初期化しました");
    }
}
Warrior.java
/**
 * 子クラス:戦士
 */
public class Warrior extends Adventurer {
    private int power;
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     * @param power 攻撃力
     */
    public Warrior(String name, int hp, int power) {
        // 親のコンストラクタを呼ぶ(必ず最初に書く)
        super(name, hp);
        
        // 子クラス独自の初期化
        this.power = power;
        System.out.println("戦士を初期化しました");
    }
}

実行例:

Warrior hero = new Warrior("勇者", 100, 20);

実行結果:

冒険者を初期化しました
戦士を初期化しました

わかりやすく言うと:

  • 親の準備作業(名前やHPの設定)を先に済ませてから、自分の準備(攻撃力の設定)をする
  • 家を建てる時、まず基礎工事(親)をしてから、内装(子)を作るイメージ

4.4 super()の重要ルール

super()の制約:

  1. 最初の行に書く - コンストラクタの1行目に記述する必要がある
  2. 1回だけ - super()とthis()は同時に使えない
  3. 暗黙的に呼ばれる - 書かない場合は自動的にsuper()が呼ばれる
public class Warrior extends Adventurer {
    private int power;
    
    // ✅ 正しい: super()が最初
    public Warrior(String name, int hp, int power) {
        super(name, hp);
        this.power = power;
    }
    
    // ❌ 間違い: super()が最初ではない
    // public Warrior(String name, int hp, int power) {
    //     this.power = power;
    //     super(name, hp);  // エラー!
    // }
}

5. 総合演習:職業派生システムの実装

これまで学んだ内容を活用して、完全な職業派生システムを実装しましょう。

手順
JobSystemというJavaプロジェクトを新規に作成し、各クラスを作成していきます
JobSystemTestクラスが実行クラスになります

job_evolution_tree_v3.png

5.1 基底クラス:冒険者

Adventurer.java
/**
 * 基底クラス:冒険者
 * すべての職業の基礎
 */
public class Adventurer {
    protected String name;
    protected int hp;
    protected int maxHp;
    protected int level;
    protected int exp;
    
    /**
     * コンストラクタ
     * @param name 名前
     * @param hp HP
     */
    public Adventurer(String name, int hp) {
        this.name = name;
        this.hp = hp;
        this.maxHp = hp;
        this.level = 1;
        this.exp = 0;
    }
    
    /**
     * 攻撃
     */
    public void attack() {
        System.out.println(name + "の攻撃!");
    }
    
    /**
     * 防御
     */
    public void defend() {
        System.out.println(name + "は身を守った!");
    }
    
    /**
     * 経験値を獲得
     * @param amount 経験値
     */
    public void gainExp(int amount) {
        exp += amount;
        System.out.println(amount + "の経験値を獲得した!");
        
        // レベルアップ判定
        while (exp >= level * 100) {
            levelUp();
        }
    }
    
    /**
     * レベルアップ
     */
    protected void levelUp() {
        level++;
        maxHp += 10;
        hp = maxHp;
        System.out.println("レベルアップ!Lv." + level + "になった!");
    }
    
    /**
     * ステータス表示
     */
    public void showStatus() {
        System.out.println("=== " + name + " のステータス ===");
        System.out.println("職業: " + getJobName());
        System.out.println("Lv: " + level);
        System.out.println("HP: " + hp + "/" + maxHp);
        System.out.println("EXP: " + exp + "/" + (level * 100));
    }
    
    /**
     * 職業名を取得(オーバーライド用)
     * @return 職業名
     */
    protected String getJobName() {
        return "冒険者";
    }
    
    // getter/setter
    public String getName() { return name; }
    public int getHp() { return hp; }
    public void setHp(int hp) { this.hp = Math.min(hp, maxHp); }
    public int getLevel() { return level; }
}

5.2 中間クラス:戦士

Warrior.java
/**
 * 中間クラス:戦士
 * 冒険者から派生
 */
public class Warrior extends Adventurer {
    protected int power;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Warrior(String name) {
        super(name, 150);  // 戦士はHPが高い
        this.power = 15;
    }
    
    /**
     * 攻撃(オーバーライド)
     */
    @Override
    public void attack() {
        System.out.println(name + "の強攻撃!威力:" + power);
    }
    
    /**
     * 斬撃
     */
    public void slash() {
        System.out.println(name + "の斬撃!威力:" + (power * 1.5));
    }
    
    /**
     * レベルアップ(オーバーライド)
     */
    @Override
    protected void levelUp() {
        super.levelUp();
        power += 5;
        System.out.println("攻撃力が上がった!");
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "戦士";
    }
    
    public int getPower() { return power; }
}

5.3 中間クラス:魔法使い

Mage.java
/**
 * 中間クラス:魔法使い
 * 冒険者から派生
 */
public class Mage extends Adventurer {
    protected int magic;
    protected int mp;
    protected int maxMp;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Mage(String name) {
        super(name, 80);  // 魔法使いはHPが低い
        this.magic = 20;
        this.mp = 50;
        this.maxMp = 50;
    }
    
    /**
     * 攻撃(オーバーライド)
     */
    @Override
    public void attack() {
        System.out.println(name + "の魔法攻撃!威力:" + magic);
    }
    
    /**
     * 魔法詠唱
     */
    public void cast() {
        if (mp >= 10) {
            mp -= 10;
            System.out.println(name + "は魔法を唱えた!威力:" + (magic * 2));
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * レベルアップ(オーバーライド)
     */
    @Override
    protected void levelUp() {
        super.levelUp();
        magic += 3;
        maxMp += 10;
        mp = maxMp;
        System.out.println("魔力が上がった!");
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "魔法使い";
    }
    
    public int getMagic() { return magic; }
    public int getMp() { return mp; }
}

5.4 派生クラス:剣士

Swordsman.java
/**
 * 派生クラス:剣士
 * 戦士から派生(高速・二刀流)
 */
public class Swordsman extends Warrior {
    private int speed;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Swordsman(String name) {
        super(name);
        this.speed = 20;
    }
    
    /**
     * 二刀流攻撃
     */
    public void dualWield() {
        System.out.println(name + "の二刀流攻撃!");
        System.out.println("右手: " + power + "ダメージ!");
        System.out.println("左手: " + power + "ダメージ!");
        System.out.println("合計: " + (power * 2) + "ダメージ!");
    }
    
    /**
     * 疾風斬り
     */
    public void swiftStrike() {
        System.out.println(name + "の疾風斬り!速度:" + speed);
        System.out.println("3連撃!威力:" + (power * 3));
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "剣士";
    }
}

5.5 派生クラス:騎士

Knight.java
/**
 * 派生クラス:騎士
 * 戦士から派生(高防御・守護)
 */
public class Knight extends Warrior {
    private int defense;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Knight(String name) {
        super(name);
        this.defense = 30;
        this.maxHp += 50;  // 騎士はさらにHPが高い
        this.hp = maxHp;
    }
    
    /**
     * 防御(オーバーライド)
     */
    @Override
    public void defend() {
        System.out.println(name + "は盾で身を守った!防御力:" + defense);
    }
    
    /**
     * 守護の構え
     */
    public void guardStance() {
        System.out.println(name + "は守護の構えをとった!");
        System.out.println("味方全員の被ダメージを" + (defense / 2) + "軽減!");
    }
    
    /**
     * シールドバッシュ
     */
    public void shieldBash() {
        System.out.println(name + "の盾攻撃!威力:" + (power + defense));
        System.out.println("敵をスタンさせた!");
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "騎士";
    }
}

5.6 派生クラス:聖職者

Healer.java
/**
 * 派生クラス:聖職者
 * 魔法使いから派生(回復・支援)
 */
public class Healer extends Mage {
    private int healPower;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Healer(String name) {
        super(name);
        this.healPower = 30;
    }
    
    /**
     * 回復魔法
     * @param target 回復対象
     */
    public void heal(Adventurer target) {
        if (mp >= 15) {
            mp -= 15;
            int healAmount = healPower + magic;
            target.setHp(target.getHp() + healAmount);
            System.out.println(name + "は回復魔法を唱えた!");
            System.out.println(target.getName() + "のHPが" + healAmount + "回復!");
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 全体回復
     */
    public void healAll() {
        if (mp >= 30) {
            mp -= 30;
            System.out.println(name + "は全体回復魔法を唱えた!");
            System.out.println("味方全員のHPが" + healPower + "回復!");
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "聖職者";
    }
}

5.7 派生クラス:魔道士

Sorcerer.java
/**
 * 派生クラス:魔導士
 * 魔法使いから派生(攻撃魔法特化)
 */
public class Sorcerer extends Mage {
    private int destructivePower;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Sorcerer(String name) {
        super(name);
        this.destructivePower = 50;
    }
    
    /**
     * 火炎魔法
     */
    public void fireball() {
        if (mp >= 20) {
            mp -= 20;
            System.out.println(name + "はファイアボールを放った!");
            System.out.println("威力: " + (magic + destructivePower));
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 雷撃魔法
     */
    public void thunder() {
        if (mp >= 25) {
            mp -= 25;
            System.out.println(name + "は雷撃を放った!");
            System.out.println("威力: " + (magic * 2 + destructivePower));
            System.out.println("敵を麻痺させた!");
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "魔導士";
    }
}

5.8 テストプログラム

JobSystemTest.java
/**
 * 職業派生システムのテスト
 */
public class JobSystemTest {
    public static void main(String[] args) {
        System.out.println("=== 職業派生システムのテスト ===\n");
        
        // 基本職業
        System.out.println("【基本職業】");
        Adventurer adventurer = new Adventurer("初心者", 100);
        adventurer.showStatus();
        adventurer.attack();
        System.out.println();
        
        // 中間職業:戦士
        System.out.println("【戦士】");
        Warrior warrior = new Warrior("戦士太郎");
        warrior.showStatus();
        warrior.attack();
        warrior.slash();
        System.out.println();
        
        // 中間職業:魔法使い
        System.out.println("【魔法使い】");
        Mage mage = new Mage("魔女");
        mage.showStatus();
        mage.attack();
        mage.cast();
        System.out.println();
        
        // 派生職業:剣士
        System.out.println("【剣士】");
        Swordsman swordsman = new Swordsman("剣士ハヤテ");
        swordsman.showStatus();
        swordsman.dualWield();
        swordsman.swiftStrike();
        System.out.println();
        
        // 派生職業:騎士
        System.out.println("【騎士】");
        Knight knight = new Knight("騎士アーサー");
        knight.showStatus();
        knight.defend();
        knight.guardStance();
        knight.shieldBash();
        System.out.println();
        
        // 派生職業:聖職者
        System.out.println("【聖職者】");
        Healer healer = new Healer("聖職者セーラ");
        healer.showStatus();
        healer.heal(knight);
        healer.healAll();
        System.out.println();
        
        // 派生職業:魔道士
        System.out.println("【魔道士】");
        Sorcerer sorcerer = new Sorcerer("魔道士ビビ");
        sorcerer.showStatus();
        sorcerer.fireball();
        sorcerer.thunder();
        System.out.println();
        
        // レベルアップのテスト
        System.out.println("【レベルアップテスト】");
        swordsman.gainExp(100);
        swordsman.showStatus();
    }
}

実行結果:

=== 職業派生システムのテスト ===

【基本職業】
=== 初心者 のステータス ===
職業: 冒険者
Lv: 1
HP: 100/100
EXP: 0/100
初心者の攻撃!

【戦士】
=== 戦士太郎 のステータス ===
職業: 戦士
Lv: 1
HP: 150/150
EXP: 0/100
戦士太郎の強攻撃!威力:15
戦士太郎の斬撃!威力:22.5

【魔法使い】
=== 魔女 のステータス ===
職業: 魔法使い
Lv: 1
HP: 80/80
EXP: 0/100
魔女の魔法攻撃!威力:20
魔女は魔法を唱えた!威力:40
MP: 40/50

【剣士】
=== 剣士ハヤテ のステータス ===
職業: 剣士
Lv: 1
HP: 150/150
EXP: 0/100
剣士ハヤテの二刀流攻撃!
右手: 15ダメージ!
左手: 15ダメージ!
合計: 30ダメージ!
剣士ハヤテの疾風斬り!速度:20
3連撃!威力:45

【騎士】
=== 騎士アーサー のステータス ===
職業: 騎士
Lv: 1
HP: 200/200
EXP: 0/100
騎士アーサーは盾で身を守った!防御力:30
騎士アーサーは守護の構えをとった!
味方全員の被ダメージを15軽減!
騎士アーサーの盾攻撃!威力:45
敵をスタンさせた!

【聖職者】
=== 聖職者セーラ のステータス ===
職業: 聖職者
Lv: 1
HP: 80/80
EXP: 0/100
聖職者セーラは回復魔法を唱えた!
騎士アーサーのHPが50回復!
MP: 35/50
聖職者セーラは全体回復魔法を唱えた!
味方全員のHPが30回復!
MP: 5/50

【魔道士】
=== 魔道士ビビ のステータス ===
職業: 魔道士
Lv: 1
HP: 80/80
EXP: 0/100
魔道士ビビはファイアボールを放った!
威力: 70
MP: 30/50
魔道士ビビは雷撃を放った!
威力: 90
敵を麻痺させた!
MP: 5/50

【レベルアップテスト】
100の経験値を獲得した!
レベルアップ!Lv.2になった!
攻撃力が上がった!
=== 剣士ハヤテ のステータス ===
職業: 剣士
Lv: 2
HP: 160/160
EXP: 100/200

実行結果から分かること:

  1. 継承の効果: 各職業クラスがAdventurerの基本機能(showStatus(), attack()など)を自動的に継承している
  2. オーバーライドの効果: 戦士のattack()は基本攻撃ではなく「強攻撃」に変更されている
  3. 独自機能の追加: 各職業が独自のメソッド(slash(), cast(), dualWield()など)を持っている
  4. レベルアップの連鎖: 親クラスのlevelUp()super.levelUp()で呼び出した後、子クラス独自の処理(攻撃力アップ)が実行されている

6. 練習問題

practice_problems_banner.png

基本問題

問題1: 継承の基本
次のコードの出力を予測してください。

class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void sound() {
        System.out.println(name + "が鳴いている");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void sound() {
        System.out.println(name + "がワンワン!");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Animal("動物");
        Dog dog = new Dog("ポチ");
        
        animal.sound();
        dog.sound();
    }
}
解答を見る

出力:

動物が鳴いている
ポチがワンワン!

理由:

  • animal.sound()はAnimalクラスのメソッドを実行
  • dog.sound()はDogクラスでオーバーライドされたメソッドを実行

問題2: superキーワード
次のコードを完成させてください。

class Vehicle {
    protected String name;
    protected int speed;
    
    public Vehicle(String name, int speed) {
        this.name = name;
        this.speed = speed;
    }
    
    public void move() {
        System.out.println(name + "が時速" + speed + "kmで移動");
    }
}

class Car extends Vehicle {
    private int fuelCapacity;
    
    // コンストラクタを完成させてください
    public Car(String name, int speed, int fuelCapacity) {
        // ここに実装
    }
    
    // move()をオーバーライドして、親のmove()を呼んだ後に
    // "燃料容量: XX L"を表示してください
    @Override
    public void move() {
        // ここに実装
    }
}
解答例を見る
public Car(String name, int speed, int fuelCapacity) {
    super(name, speed);
    this.fuelCapacity = fuelCapacity;
}

@Override
public void move() {
    super.move();
    System.out.println("燃料容量: " + fuelCapacity + " L");
}

応用問題

問題3: 職業の追加
Mageクラスを継承して、「賢者(Sage)」クラスを実装してください。

要件:

  • 回復魔法と攻撃魔法の両方が使える
  • heal()メソッド: MPを20消費して味方を回復
  • explosion()メソッド: MPを40消費して強力な攻撃魔法
解答例を見る
/**
 * 派生クラス:賢者
 * 魔法使いから派生(回復と攻撃の両立)
 */
public class Sage extends Mage {
    private int wisdom;
    
    /**
     * コンストラクタ
     * @param name 名前
     */
    public Sage(String name) {
        super(name);
        this.wisdom = 25;
        this.maxMp += 20;  // 賢者はMPが多い
        this.mp = maxMp;
    }
    
    /**
     * 回復魔法
     * @param target 回復対象
     */
    public void heal(Adventurer target) {
        if (mp >= 20) {
            mp -= 20;
            int healAmount = magic + wisdom;
            target.setHp(target.getHp() + healAmount);
            System.out.println(name + "は回復魔法を唱えた!");
            System.out.println(target.getName() + "のHPが" + healAmount + "回復!");
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 爆発魔法
     */
    public void explosion() {
        if (mp >= 40) {
            mp -= 40;
            System.out.println(name + "は爆発魔法を唱えた!");
            System.out.println("威力: " + (magic * 3 + wisdom * 2));
            System.out.println("MP: " + mp + "/" + maxMp);
        } else {
            System.out.println("MPが足りない!");
        }
    }
    
    /**
     * 職業名(オーバーライド)
     */
    @Override
    protected String getJobName() {
        return "賢者";
    }
}

問題4: ポリモーフィズムの活用
次のメソッドを実装してください。

/**
 * パーティ全員に攻撃させる
 * @param party パーティメンバー
 */
public void attackAll(Adventurer[] party) {
    // ここに実装
}
解答例を見る
/**
 * パーティ全員に攻撃させる
 * @param party パーティメンバー
 */
public void attackAll(Adventurer[] party) {
    for (Adventurer member : party) {
        member.attack();  // 各クラスのオーバーライドされたattack()が呼ばれる
    }
}

// 使用例
public static void main(String[] args) {
    Adventurer[] party = {
        new Warrior("戦士"),
        new Mage("魔法使い"),
        new Swordsman("剣士"),
        new Knight("騎士")
    };
    
    attackAll(party);
}

ポイント:

  • 親クラス型の配列で、すべての子クラスを扱える
  • 各オブジェクトの実際の型に応じたメソッドが実行される(これをポリモーフィズムと言います)

問題5: 継承の設計
「モンスター」のクラス階層を設計してください。

要件:

  • 基底クラス: Monster
  • 中間クラス: Dragon, Undead, Beast
  • 各中間クラスから2つずつ派生クラスを作成
解答例を見る
/**
 * 基底クラス:モンスター
 */
public class Monster {
    protected String name;
    protected int hp;
    protected int attack;
    
    public Monster(String name, int hp, int attack) {
        this.name = name;
        this.hp = hp;
        this.attack = attack;
    }
    
    public void attack() {
        System.out.println(name + "の攻撃!威力:" + attack);
    }
    
    public String getName() { return name; }
}

// ドラゴン系
public class Dragon extends Monster {
    protected int fireBreath;
    
    public Dragon(String name) {
        super(name, 300, 50);
        this.fireBreath = 80;
    }
    
    public void breathFire() {
        System.out.println(name + "は火を吐いた!威力:" + fireBreath);
    }
}

public class RedDragon extends Dragon {
    public RedDragon() {
        super("レッドドラゴン");
        this.fireBreath = 100;
    }
}

public class IceDragon extends Dragon {
    public IceDragon() {
        super("アイスドラゴン");
    }
    
    @Override
    public void breathFire() {
        System.out.println(name + "は冷気を吐いた!威力:" + fireBreath);
    }
}

// アンデッド系
public class Undead extends Monster {
    protected int curse;
    
    public Undead(String name) {
        super(name, 150, 30);
        this.curse = 20;
    }
    
    public void curseAttack() {
        System.out.println(name + "の呪い攻撃!威力:" + curse);
    }
}

public class Skeleton extends Undead {
    public Skeleton() {
        super("スケルトン");
    }
}

public class Vampire extends Undead {
    public Vampire() {
        super("ヴァンパイア");
        this.curse = 40;
    }
    
    public void lifeSteal() {
        System.out.println(name + "は生命力を吸収した!");
    }
}

// 獣系
public class Beast extends Monster {
    protected int clawAttack;
    
    public Beast(String name) {
        super(name, 100, 25);
        this.clawAttack = 35;
    }
    
    public void claw() {
        System.out.println(name + "の爪攻撃!威力:" + clawAttack);
    }
}

public class Wolf extends Beast {
    public Wolf() {
        super("ウルフ");
    }
}

public class Bear extends Beast {
    public Bear() {
        super("ベア");
        this.hp = 150;
        this.clawAttack = 50;
    }
}

summary_banner.png

本記事では、継承の基礎から実践的な使い方まで解説しました。

重要ポイント

1.継承とは

  • 既存クラスの機能を引き継いで新しいクラスを作る
  • コードの重複を避け、保守性と拡張性を向上
  • is-a関係を表現(戦士冒険者である)

2.extendsキーワード

  • class 子 extends 親で継承
  • 親のフィールド・メソッドを自動的に継承
  • protectedで子クラスからアクセス可能

3.オーバーライド

  • 親のメソッドを子クラスで再定義
  • @Overrideアノテーションで安全性向上
  • シグネチャ(メソッド名、引数)は同じにする

4.superキーワード

  • super.メソッド(): 親のメソッドを呼ぶ
  • super(): 親のコンストラクタを呼ぶ
  • コンストラクタの最初に記述

5.設計指針

  • 共通部分は親クラスに
  • 独自機能は子クラスに
  • クラスの親子関係は浅く保つ

next_episode_preview_banner-1.png

次回は「第8回:ポリモーフィズム」をお届けします!

  • ポリモーフィズムとは何か(同じ命令でも、相手によって動きが変わる仕組み)
  • アップキャストとダウンキャスト
  • instanceof演算子
  • パーティシステムの実装

お楽しみに!


reference_materials_banner-1.png

公式ドキュメント:

書籍:

  • Effective Java (Third Edition) - Joshua Bloch
    • Item 18: Favor composition over inheritance

series_list_banner-1.png

Java Quest - オブジェクト指向の冒険シリーズ


© 2024 Java Quest - オブジェクト指向って何?RPGで理解する超入門
All Rights Reserved.

本記事は教育目的で作成されています。RPGの例を通じてプログラミング概念を楽しく学習できることを目指しています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?