0
0

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で理解する超入門 (ポリモーフィズム)

0
Last updated at Posted at 2025-12-28

title_banner_v3.png

はじめに

プログラミング講師をしていると、「配列で職業ごとに別々に管理していたら、コードが複雑になりすぎました」「キャストしようとしたらClassCastExceptionが出ました」という相談を頻繁に受けます。これはポリモーフィズムの概念を理解していないために起こる、初心者が必ず一度は通る壁です。

本記事では、RPGゲームの「パーティ管理システム」を例に、ポリモーフィズムの基本から実践的な使い方まで徹底的に解説します。戦士・魔法使い・騎士・剣士など多様な職業を、一つの型で統一的に扱えるようになりましょう。

この記事で学べること

  • ポリモーフィズム(多態性) の概念と利点
  • アップキャスト :子から親への型変換
  • ダウンキャスト :親から子への型変換
  • instanceof演算子の使い方
  • パーティ管理システムの実装
  • 動的バインディングの仕組み

対象読者

  • 継承の基礎を理解している方
  • クラス階層を扱った経験がある方
  • 型変換でエラーに遭遇した方
  • コードをシンプルにしたい方

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

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

1. ポリモーフィズムとは何か?

1.1 ポリモーフィズムの基本概念

ポリモーフィズム(Polymorphism) は、「多態性」「多様性」を意味し、一つの型で多様なオブジェクトを扱える仕組みです。オブジェクト指向プログラミングの三大要素の一つで、継承と組み合わせることで真価を発揮します。

RPGでも同じです

polymorphism_concept_v2.png

三大要素の復習:

  1. カプセル化(Encapsulation) - データを保護する
  2. 継承(Inheritance) - コードを再利用する
  3. ポリモーフィズム(Polymorphism) - 柔軟に扱う ← 今回

1.2 なぜポリモーフィズムが必要なのか?

ポリモーフィズムがない場合の問題

手順
BadPartyプロジェクトを作成し、srcフォルダに各クラスを作成します

Warrior.java
/**
 * 勇者クラス
 */
public class Warrior {
   // いまはこれだけでOK
}
Mage.java
/**
 * 魔法使いクラス
 */
public class Mage {
   // いまはこれだけでOK
}
Knight.java
/**
 * 騎士クラス
 */
public class Knight {
   // いまはこれだけでOK
}
Swordsman.java
/**
 * 剣士クラス
 */
public class Swordsman {
   // いまはこれだけでOK
}

実行クラス
BadPartyManagementクラスを作成し、Javaアプリケーションの実行を行います

BadPartyManagement.java
/**
 * ポリモーフィズムなしの管理(悪い例)
 * ポリモーフィズムを使わずに if/switch や instanceof で振る舞いを分岐している
 */
public class BadPartyManagement {
    public static void main(String[] args) {
		// 職業ごとに別々の配列
		Warrior[] warriors = new Warrior[10];
		Mage[] mages = new Mage[10];
		Knight[] knights = new Knight[10];
		Swordsman[] swordsmen = new Swordsman[10];

		// 職業ごとにキャラを作成
		warriors[0] = new Warrior();
		mages[0] = new Mage();
		knights[0] = new Knight();
		swordsmens[0] = new Swordsman();

		// 全員に攻撃させる(重複コード)
		for (Warrior warrior : warriors) {
			if (warrior != null) doAattack(warrior);
		}
		for (Mage mage : mages) {
			if (mage != null) doAattack(mage);
		}
		for (Knight knight : knights) {
			if (knight != null) doAattack(knight);
		}
		for (Swordsman swordsmen : swordsmens) {
			if (swordsmen != null) doAattack(swordsmen);
		}
    }

	/**
	 * 攻撃実行メソッド
	 * 悪い例:職業の種類ごとにif/elseで処理を分けている
	 * @param character
	 */
	private static void doAattack(Object character) {
		
		if(character instanceof Warrior) {
			System.out.println( "勇者の盾攻撃!");
			
		} else if(character instanceof Mage) {
			System.out.println("魔法使いの魔法攻撃!");
			
		} else if(character instanceof Knight) {
			System.out.println("騎士の盾攻撃!");
			
		} else if(character instanceof Swordsman) {
			System.out.println("剣士の二刀流攻撃!");
			
		} else {
			System.out.println("通常の攻撃");
		}
}

実行結果

勇者の盾攻撃!
魔法使いの魔法攻撃!
騎士の盾攻撃!
剣士の二刀流攻撃!

なぜこれは悪いか(問題点):

  • 職業ごとに配列が必要(可読性が低い)
  • 新しい職業を追加するたびにifを修正する必要がある(オープン/クローズド原則違反)
  • 条件分岐が増えると複雑になりバグを生みやすい(保守性が低い)
  • Object を受け取ったりキャストしたりしているのでミスが起きやすい(型安全でない)
  • コードが長く複雑になる(可読性が低い)
  • 振る舞いが複数箇所に分散してしまう(責務の分離ができていない)

1.3 ポリモーフィズムを使った解決策

手順
GoodPartyプロジェクトを作成し、srcフォルダに各クラスを作成します

Adventurer.java
/**
 * 冒険者クラス(親クラス)
 */
public abstract class Adventurer {
	/**
	 * 攻撃処理(子クラスで実装すること)
	 */
	abstract void attack();
}
Warrior.java
/**
 * 勇者クラス
 */
public class Warrior extends Adventurer {
	@Override
	public void attack() {
		System.out.println( "勇者の盾攻撃!");
	}
}
Mage.java
/**
 * 魔法使いクラス
 */
public class Mage extends Adventurer {
	@Override
	public void attack() {
		System.out.println("魔法使いの魔法攻撃!");
	}
}
Knight.java
/**
 * 騎士クラス
 */
public class Knight extends Adventurer{
	@Override
	public void attack() {
		System.out.println("騎士の盾攻撃!");
	}
}
Swordsman.java
/**
 * 剣士クラス
 */
public class Swordsman extends Adventurer {
	@Override
	public void attack() {
		System.out.println("剣士の二刀流攻撃!");
	}
}

実行クラス
GoodPartyManagementクラスを作成し、Javaアプリケーションの実行を行います

GoodPartyManagement.java
/**
 * ポリモーフィズムありの管理(良い例)
 */
public class GoodPartyManagement {
	public static void main(String[] args) {
        // 親クラス型の配列で一括管理
        Adventurer[] party = {
        	new Warrior(), new Mage(), new Knight(), new Swordsman()
        };
                
        // 全員に攻撃させる(シンプル!)
        for (Adventurer member : party) {
            if(member != null) doAttack(member);  
        }
    }
	
	/**
	 * 攻撃実行
	 * @param member
	 */
	private static void doAttack(Adventurer member) {
		member.attack();
	}
}

実行結果

勇者の盾攻撃!
魔法使いの魔法攻撃!
騎士の盾攻撃!
剣士の二刀流攻撃!

改善された点:

  • ✅ 一つの配列で全職業を管理(保守性が向上)
  • ✅ 振る舞いはそれぞれのクラスが持つので、共通処理は1回書くだけ(単一責任)
  • ✅ 新しい職業を追加してもコード変更不要(拡張容易)
  • ✅ 条件分岐が消え、コードがシンプルで読みやすい(可読性・保守性が向上)
  • ✅ Adventurer 型で扱えるためキャスト不要(型安全)

1.4 まとめ

  • if/instanceof/switch による分岐は「悪臭(コードスメル)」になりがち
    まずはポリモーフィズムで振る舞いを分散させることを考える
  • 小さくて単純なプログラムならデータ駆動(フィールド)で十分な場合もあるが、拡張性を重視するならポリモーフィズムを使うべき。

2. アップキャスト:子から親へ

2.1 アップキャストとは

アップキャスト(Upcast)は、子クラス型のオブジェクトを親クラス型の変数に代入することです。これは自動的に行われ、安全な操作です。

upcast_v2.png

2.2 アップキャストの基本

手順
UpcastDemoプロジェクトを作成し、srcフォルダに各クラスを作成します

Adventurer.java
/**
 * 冒険者クラス(親クラス)
 */
public class Adventurer {

	/**
     * 攻撃する
     */
    public void attack() {
        System.out.println("冒険者の攻撃!");
    }
	
	 /**
     * 防御する
     */
    public void defend() {
        System.out.println("冒険者は防御した!");
    }
}
Warrior.java
/**
 * 勇者クラス
 */
public class Warrior extends Adventurer {
	@Override
	public void attack() {
		System.out.println( "勇者の盾攻撃!");
	}
    
    /**
     * 斬撃(戦士独自のメソッド)
     */
	public void slash(){
		System.out.println("勇者の斬撃!");
	}
}

実行クラス
UpcastDemoクラスを作成し、Javaアプリケーションの実行を行います

UpcastDemo.java
/**
 * アップキャストのデモ
 */
public class UpcastDemo {
    public static void main(String[] args) {
    
        // 子クラスのオブジェクトを作成
        Warrior warrior = new Warrior();
        
        // アップキャスト(自動)
        Adventurer adv = warrior;  // キャスト不要!
        
        // 親クラスのメソッドは使える
        adv.defend();   // OK
        
        // 子クラスのメソッドは使えない
        // adv.slash();  // コンパイルエラー!
        
        // 型確認
        System.out.println(warrior instanceof Warrior);     // true
        System.out.println(warrior instanceof Adventurer);  // true
        System.out.println(adv instanceof Warrior);         // true
    }
}

実行結果

冒険者は防御した!
true
true
true

2.3 アップキャストの制約

アップキャスト後は、子クラス固有のメンバーにアクセスできなくなります。

手順
UpcastLimitationプロジェクトを作成し、srcフォルダに各クラスを作成します

Adventurer.java
/**
 * 冒険者クラス(親クラス)
 */
public class Adventurer {

	/**
     * 攻撃する
     */
    public void attack() {
        System.out.println("冒険者の攻撃!");
    }
	
    /**
     * 防御する
     */
    public void defend() {
        System.out.println("冒険者は防御した!");
    }
}
Warrior.java
/**
 * 勇者クラス
 */
class Warrior extends Adventurer {
	protected int power = 100;
	private int hp;
    
	@Override
	public void attack() {
		System.out.println( "勇者の盾攻撃!");
	}

    /**
     * 斬撃(戦士独自のメソッド)
     */
	public void slash(){
		System.out.println("勇者の斬撃!");
	}  
    
	//---getter/setter
	public void getPower() {
		System.out.println("勇者の攻撃力は" + this.power);
	}
    
	public int getHp() {
		return hp;
	}

	public void setHp(int hp) {
		this.hp = hp;
	}
}

実行クラス
UpcastLimitationクラスを作成し、Javaアプリケーションの実行を行います

UpcastLimitation.java
public class UpcastLimitation {
    public static void main(String[] args) {
        Warrior warrior = new Warrior();
        
        // 子クラス型:全てのメンバーにアクセス可能
        warrior.defend();   // OK: 親のメソッド → 冒険者は防御した!
        warrior.attack();   // OK: 子のメソッド → 勇者の盾攻撃!
        warrior.slash();    // OK: 子のメソッド → 勇者の斬撃!
        warrior.setHp(100); // OK: 子のフィールド → HPの設定:出力なし
        
        // アップキャスト:変数は Adventurer だが、実体は Warrior
        // オブジェクトの代入は住所(参照)なので、住所から参照した実体は Warrior2
        Adventurer adv = warrior;
        System.out.println();
        
        // 親クラス型:親のメンバーのみアクセス可能
        adv.defend(); // OK: 親のメソッド → 冒険者は防御した!
        adv.attack(); // OK: 子のメソッド → 勇者の盾攻撃! ※実体の attack が呼ばれる(動的ディスパッチ)
        // adv.slash();     // コンパイルエラー: Adventurer型には slash がないから呼べない
        // adv.setHp(100);  // コンパイルエラー: Adventurer型には setHp がないから呼べない
    }
}

実行結果

冒険者は防御した!
勇者の盾攻撃!
勇者の斬撃!

冒険者は防御した!
勇者の盾攻撃!

重要ポイント

  • 変数の型(コンパイル時に見られるもの)と、実体の型(実行時にどのオブジェクトか)は別物
  • 親クラスの変数に子クラスのインスタンスを入れても、実際(実体)は子クラスのまま
  • ただし、変数の見た目(扱う型)は親クラスになるので、変数から呼べる操作は親で定義されたものだけ
  • 実際に実行されるメソッドは、オブジェクトの実際の型に基づいて動的ディスパッチで決まる

3. ダウンキャスト:親から子へ

3.1 ダウンキャストとは

ダウンキャスト(Downcast)は、親クラス型の変数を子クラス型に変換することです。これは明示的なキャストが必要で、誤った型にキャストするとエラーになります。

downcast_v2.png

3.2 ダウンキャストの基本

  • 冒険者クラスはアップキャストで作成したものを使用
  • 勇者クラスはアップキャストで作成したものを使用

手順
UpcastLimitationプロジェクトのsrcフォルダにDowncastDemoクラスを追加し、実行します

DowncastDemo.java
/**
 * ダウンキャストのデモ
 */
public class DowncastDemo {
	public static void main(String[] args) {
        // アップキャスト 
        // 実体(本当の中身) → Warrior  型(変数の箱) → Adventurer
        // イメージ:「勇者(Warrior)を『冒険者(Adventurer)』という大きな箱に入れている」
        // そのため adv では attack()、defend()のような 親クラスにある機能しか見えない
        Adventurer adv = new Warrior();
        
        // ダウンキャスト
        // イメージ:「冒険者の箱を開けて、中身が本当に勇者ならもう一度『勇者専用の箱』に入れ直す」
        // 中身は最初から Warrior、でも 変数の型が Adventurer だった
        // だから 明示的にキャスト(型変換)する必要がある
        Warrior warrior = (Warrior) adv;  

        // ダウンキャストすると、子クラス(Warrior)専用のメソッドが使える
        warrior.slash();     // OK
        warrior.setHp(100);  // OK
        warrior.getPower();  // OK
    }
}

実行結果

勇者の斬撃!
勇者の攻撃力は100

なぜ「キャスト」が必須なの?

Warrior warrior = adv;   // ← これはエラー

Javaはこう考えます

「adv は冒険者かもしれないし、
魔法使いかもしれないよね?
本当に勇者って保証できる?」

だから Java は
「自分で責任を持って変換するなら (Warrior) を書きなさい」
と要求します

3.3 ダウンキャストの失敗例

手順
UpcastLimitationプロジェクトのsrcフォルダに各クラスを追加します

Mage.java
/**
 * 魔法使いクラス
 */
public class Mage extends Adventurer {
	protected int magic;
    
	/**
	 * コンストラクタ
	 */
	public Mage() {
		this.magic = 20;
	}
    
	@Override
	public void attack() {
		System.out.println( "魔法使いの攻撃!");
	}
    
    /**
     * 魔法詠唱(魔法使い独自のメソッド)
     */
    public void cast() {
        System.out.println("魔法使いは魔法を唱えた!");
    }
    
    //--- getter/setter
	public int getMagic() {
		return this.magic ;
	}

	public void setMagic(int magic) {
		this.magic = magic;
	}
}    
Knight.java
public class Knight extends Adventurer{
	private int defense;
    /**
     * 守護の構え
     */
    public void guardStance() {
        System.out.println("騎士は守護の構えをとった!");
        System.out.println("味方全員の被ダメージを" + (defense / 2) + "軽減!");
    }
    
    /**
     * コンストラクタ
     */
	public Knight() {
		this.defense = 30;
	}
}

実行クラス DowncastFailureクラスを作成し、実行します

DowncastFailure.java
/**
 * ダウンキャストの失敗例
 */
public class DowncastFailure {
    public static void main(String[] args) {
    
        // 実体は Mageオブジェクト
        Adventurer adv = new Mage();
        
        try {
            // Warriorにダウンキャストしようとする
            // イメージ:「冒険者の箱を開けて、中身が本当に勇者ならもう一度『勇者専用の箱』に入れ直す」
            // しかし中身は Mage なので Warrior の箱には入らなかった
            Warrior warrior = (Warrior) adv;  // 実行時エラーが発生します
            warrior.slash();
            
        } catch (ClassCastException e) {
            System.out.println("エラー: " + e.getMessage());
            // ClassCastException(実行時エラー) 
            // エラー: Mage cannot be cast to Warrior
        }
    }
}

実行結果

エラー: class Mage cannot be cast to class Warrior (Mage and Warrior are in unnamed module of loader 'app')

3.4 ダウンキャストが必要な場面

ダウンキャストが必要になるのは「親クラス型として扱っているオブジェクトを、子クラス独自の機能を使いたいとき」です

手順
UpcastLimitationプロジェクトのsrcフォルダにDowncastUseCaseクラスを作成し、実行します

DowncastUseCase.java
/**
 * ダウンキャストが必要な場面
 */
public class DowncastUseCase {
    public static void main(String[] args) {
        Adventurer[] party = {
            new Warrior(), new Mage(), new Knight()
        };
        
        // 職業ごとに特殊な処理をしたい
        for (Adventurer member : party) {
            // 共通処理
            member.attack();
            
            // 職業固有の処理(ダウンキャスト必要)
            if (member instanceof Warrior) {
                Warrior w = (Warrior) member;
                w.slash();  // 戦士専用
                
            } else if (member instanceof Mage) {
                Mage m = (Mage) member;
                m.cast();   // 魔法使い専用
                
            } else if (member instanceof Knight) {
                Knight k = (Knight) member;
                k.guardStance();  // 騎士専用
            }
        }
    }
}

実行結果

勇者の盾攻撃!
勇者の斬撃!

魔法使いの攻撃!
魔法使いは魔法を唱えた!

冒険者の攻撃!
騎士は守護の構えをとった!
味方全員の被ダメージを15軽減!

まとめ

実務ではポリモーフィズム設計の見直しでダウンキャストを減らすのが基本です

項目 内容
ダウンキャストはいつ? 子クラス専用の機能を使いたいとき
いつも必要? ほとんど不要
安全に使うには? instanceof で確認
よくある誤解 「使わないといけない」→ ✕

4. instanceof演算子:安全な型チェック

4.1 instanceof演算子とは

instanceof演算子は、オブジェクトが特定のクラスのインスタンスかどうかを判定します。ダウンキャスト前の安全確認に必須です。

instanceof_operator_v2.png

4.2 instanceofの基本構文

オブジェクト instanceof クラス名

戻り値はboolean(true または false)
「中身がそのクラス(または子クラス)か?」を調べる

4.3 instanceofの使用例

手順
UpcastLimitationプロジェクトのsrcフォルダにInstanceofDemoクラスを作成し、実行します

InstanceofDemo.java
/**
 * instanceof演算子のデモ
 */
public class InstanceofDemo {
    public static void main(String[] args) {
        Warrior warrior = new Warrior();
        
        // 型チェック
        System.out.println(warrior instanceof Warrior);    // true
        System.out.println(warrior instanceof Adventurer); // true
        // System.out.println(warrior instanceof Mage);    // エラー:非互換条件オペランド型
        System.out.println(warrior instanceof Object);     // true
        
        // nullのチェック
        Adventurer nullAdv = null;
        System.out.println(nullAdv instanceof Warrior);     // false
    }
}

実行結果

true
true
true
false

補足
instanceof で 「非互換条件オペランド型」 というエラーになるのは、
「そのオブジェクトが、構造的に“なり得ない”クラスを調べているとき」 です

Javaは

「絶対に true にならないチェックは書かせない」
 というルールを持っています。

4.4 安全なダウンキャスト

手順
UpcastLimitationプロジェクトのsrcフォルダにSafeDowncastクラスを作成し、実行します

SafeDowncast.java
/**
 * instanceofを使った安全なダウンキャスト
 */
public class SafeDowncast {
    /**
     * パーティメンバーに特殊行動をさせる
     * @param member パーティメンバー
     */
    public static void specialAction(Adventurer member) {
        // nullチェック
        if (member == null) {
            System.out.println("メンバーがいません");
            return;
        }
        
        // 型を確認してからダウンキャスト
        if (member instanceof Warrior) {
            Warrior w = (Warrior) member;
            w.slash();
            System.out.println("HP: " + w.getHp());
            
        } else if (member instanceof Mage) {
            Mage m = (Mage) member;
            m.cast();
            System.out.println("魔力: " + m.getMagic());
            
        } else if (member instanceof Knight) {
            Knight k = (Knight) member;
            k.guardStance();
            
        } else {
            System.out.println("特殊行動なし");
        }
    }
    
    public static void main(String[] args) {
        specialAction(new Warrior());
        specialAction(new Mage());
        specialAction(new Knight());
    }
}

実行結果

勇者の斬撃!
HP: 0
魔法使いは魔法を唱えた!
魔力: 20
騎士は守護の構えをとった!
味方全員の被ダメージを15軽減!

4.5 instanceofの注意点

1. 継承階層での判定

Swordsman.java
/**
 * 派生クラス:剣士
 * 戦士から派生(高速・二刀流)
 */
public class Swordsman extends Warrior {
	private int speed;
    
    /**
     * 疾風斬り
     */
    public void swiftStrike() {
        System.out.println("の疾風斬り!速度:" + speed);
        System.out.println("3連撃!威力:" + (power * 3));
    }
}
InstanceofHierarchy.java
public class InstanceofHierarchy {
    public static void main(String[] args) {
        Swordsman swordsman = new Swordsman();
        
        // Swordsmanは、Warrior、Adventurer、Objectのインスタンス
        System.out.println(swordsman instanceof Swordsman);   // true
        System.out.println(swordsman instanceof Warrior);     // true
        System.out.println(swordsman instanceof Adventurer);  // true
        System.out.println(swordsman instanceof Object);      // true
        
        // 非互換条件オペランド型 Swordsman と Mage
        // System.out.println(swordsman instanceof Mage);    // コンパイルエラー   
    }
}

実行結果

true
true
true
true

2. nullは常にfalse

InstanceofNull.java
public class InstanceofNull {
    public static void main(String[] args) {
        Adventurer adv = null;
        
        // nullはどのクラスでもfalse
        System.out.println(adv instanceof Warrior);     // false
        System.out.println(adv instanceof Adventurer);  // false
        System.out.println(adv instanceof Object);      // false
        
        // nullチェックとinstanceofの組み合わせ
        if (adv != null && adv instanceof Warrior) {
            // 安全
        }
    }
}

実行結果

false
false
false

5. 動的バインディング

5.1 動的バインディングとは

動的バインディング(Dynamic Binding) は、実行時にオブジェクトの実際の型に基づいてメソッドが決定される仕組みです。ポリモーフィズムの核心部分です。

DynamicBindingDemo.java
/**
 * 動的バインディングのデモ
 */
public class DynamicBindingDemo {
    public static void main(String[] args) {
        // 親クラス型で宣言
        Adventurer adv1 = new Warrior();
        Adventurer adv2 = new Mage();
        Adventurer adv3 = new Knight();
        
        // 同じメソッド呼び出しでも、実際の型に応じたメソッドが実行される
        adv1.attack();  // Warriorのattack()が実行
        adv2.attack();  // Mageのattack()が実行
        adv3.attack();  // Knightのattack()が実行
    }
}

実行結果

勇者の盾攻撃!
魔法使いの攻撃!
冒険者の攻撃!

5.2 静的バインディングとの違い

/**
 * 静的バインディング vs 動的バインディング
 */
public class BindingComparison {
    public static void main(String[] args) {
        Warrior warrior = new Warrior();
        Adventurer adv = warrior;
        
        // 動的バインディング:実際の型(Warrior)のメソッドが実行
        adv.attack();  // Warriorのattack()
        
        // フィールドアクセスは静的(変数の型に基づく)
        // adv.power;  // エラー: Adventurer型にpowerはない
    }
}

実行結果

勇者の盾攻撃!

バインディングの種類:

種類 決定時期 対象
静的バインディング コンパイル時 フィールド、static、final、private
動的バインディング 実行時 オーバーライドされたメソッド

6. 総合演習:パーティ管理システム

これまで学んだ内容を活用して、完全なパーティ管理システムを実装しましょう。

party_system_v2.png

6.1 各キャラクターを作成

冒険者クラス (Adventurer)
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; }
}
戦士クラス (Warrior)
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; 
    }
    public void setPower(int newPower) {
        this.power = newPower;
    }
}
騎士クラス (Knight)
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 "騎士";
    }
}
魔法使いクラス (Mage)
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; 
    }
    
   	public void setMagic(int newMagic) {
		this.magic = newMagic;
	}
	public void setMp(int newMagic) {
		this.mp = newMagic;
	}
}
剣士クラス (Swordsman)
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 "剣士";
    }
 }

6.2 Partyクラス

Party.java
import java.util.ArrayList;
import java.util.List;

/**
 * パーティ管理クラス
 */
public class Party {
    private String name;
    private List<Adventurer> members;
    private int maxSize;
    
    /**
     * コンストラクタ
     * @param name パーティ名
     * @param maxSize 最大人数
     */
    public Party(String name, int maxSize) {
        this.name = name;
        this.maxSize = maxSize;
        this.members = new ArrayList<>();
    }
    
    /**
     * メンバーを追加
     * @param member 追加するメンバー
     * @return 追加成功ならtrue
     */
    public boolean addMember(Adventurer member) {
        if (member == null) {
            System.out.println("nullのメンバーは追加できません");
            return false;
        }
        
        if (members.size() >= maxSize) {
            System.out.println("パーティが満員です");
            return false;
        }
        
        members.add(member);
        System.out.println(member.getName() + "がパーティに加わった!");
        return true;
    }
    
    /**
     * メンバーを削除
     * @param member 削除するメンバー
     * @return 削除成功ならtrue
     */
    public boolean removeMember(Adventurer member) {
        if (members.remove(member)) {
            System.out.println(member.getName() + "がパーティを離れた");
            return true;
        }
        return false;
    }
    
    /**
     * 全員に攻撃させる(ポリモーフィズム)
     */
    public void attackAll() {
        System.out.println("=== " + name + "の一斉攻撃! ===");
        for (Adventurer member : members) {
            member.attack();
        }
    }
    
    /**
     * 全員のステータスを表示
     */
    public void showAllStatus() {
        System.out.println("=== " + name + "のメンバー ===");
        for (Adventurer member : members) {
            member.showStatus();
            System.out.println();
        }
    }
    
    /**
     * 全員のHPを回復
     * @param amount 回復量
     */
    public void healAll(int amount) {
        System.out.println("=== パーティ全体回復 ===");
        for (Adventurer member : members) {
            int currentHp = member.getHp();
            member.setHp(currentHp + amount);
            System.out.println(member.getName() + "のHPが" + amount + "回復!");
        }
    }
    
    /**
     * 全員に経験値を配分
     * @param totalExp 総経験値
     */
    public void distributeExp(int totalExp) {
        if (members.isEmpty()) {
            return;
        }
        
        int expPerMember = totalExp / members.size();
        System.out.println("=== 経験値を配分 ===");
        for (Adventurer member : members) {
            member.gainExp(expPerMember);
        }
    }
    
    /**
     * 職業ごとの特殊行動
     */
    public void specialActions() {
        System.out.println("=== 職業固有の特殊行動 ===");
        for (Adventurer member : members) {
            System.out.print(member.getName() + ": ");
            
            // instanceofで型を確認してダウンキャスト
            if (member instanceof Swordsman) {
                Swordsman s = (Swordsman) member;
                s.dualWield();
                
            } else if (member instanceof Knight) {
                Knight k = (Knight) member;
                k.guardStance();
                
            } else if (member instanceof WhiteMage) {
                WhiteMage wm = (WhiteMage) member;
                wm.healAll();
                
            } else if (member instanceof BlackMage) {
                BlackMage bm = (BlackMage) member;
                bm.fireball();
                
            } else if (member instanceof Warrior) {
                Warrior w = (Warrior) member;
                w.slash();
                
            } else if (member instanceof Mage) {
                Mage m = (Mage) member;
                m.cast();
                
            } else {
                System.out.println("特殊行動なし");
            }
        }
    }
    
    /**
     * 戦士の数を数える
     * @return 戦士の数
     */
    public int countWarriors() {
        int count = 0;
        for (Adventurer member : members) {
            if (member instanceof Warrior) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * 魔法使いの数を数える
     * @return 魔法使いの数
     */
    public int countMages() {
        int count = 0;
        for (Adventurer member : members) {
            if (member instanceof Mage) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * パーティ情報を表示
     */
    public void displayPartyInfo() {
        System.out.println("=== " + name + " ===");
        System.out.println("メンバー数: " + members.size() + "/" + maxSize);
        System.out.println("戦士系: " + countWarriors() + "人");
        System.out.println("魔法使い系: " + countMages() + "人");
    }
    
    // getter
    public String getName() { return name; }
    public List<Adventurer> getMembers() { return new ArrayList<>(members); }
    public int getSize() { return members.size(); }
}

6.3 テストプログラム

実行クラス
PartySystemTestクラスを作成し、実行します

PartySystemTest.java
/**
 * パーティ管理システムのテスト
 */
public class PartySystemTest {
    public static void main(String[] args) {
        System.out.println("=== パーティ管理システムのテスト ===\n");
        
        // パーティを作成
        Party party = new Party("冒険者の党", 6);
        
        // メンバーを追加(ポリモーフィズム)
        party.addMember(new Warrior("勇者"));
        party.addMember(new Mage("魔女"));
        party.addMember(new Knight("騎士アーサー"));
        party.addMember(new Swordsman("剣士ハヤテ"));
        party.addMember(new WhiteMage("白魔道士セーラ"));
        party.addMember(new BlackMage("黒魔道士ビビ"));
        System.out.println();
        
        // パーティ情報表示
        party.displayPartyInfo();
        System.out.println();
        
        // 全員のステータス表示
        party.showAllStatus();
        
        // 一斉攻撃(ポリモーフィズム)
        party.attackAll();
        System.out.println();
        
        // 職業固有の特殊行動(instanceof + ダウンキャスト)
        party.specialActions();
        System.out.println();
        
        // 全員回復
        party.healAll(30);
        System.out.println();
        
        // 経験値配分
        party.distributeExp(600);
        System.out.println();
        
        // メンバー追加・削除のテスト
        System.out.println("【メンバー管理のテスト】");
        Adventurer newMember = new Warrior("新人戦士");
        party.addMember(newMember);  // 満員なので失敗
        
        Adventurer removedMember = party.getMembers().get(0);
        party.removeMember(removedMember);
        party.addMember(newMember);  // 今度は成功
        System.out.println();
        
        party.displayPartyInfo();
    }
}

実行結果

=== パーティ管理システムのテスト ===

勇者がパーティに加わった!
魔女がパーティに加わった!
騎士アーサーがパーティに加わった!
剣士ハヤテがパーティに加わった!

=== 冒険者の党 ===
メンバー数: 4/4
戦士系: 3人
魔法使い系: 1人

=== 冒険者の党のメンバー ===
=== 勇者 のステータス ===
職業: 戦士
Lv: 1
HP: 150/150
EXP: 0/100

=== 魔女 のステータス ===
職業: 魔法使い
Lv: 1
HP: 80/80
EXP: 0/100

=== 騎士アーサー のステータス ===
職業: 騎士
Lv: 1
HP: 200/200
EXP: 0/100

=== 剣士ハヤテ のステータス ===
職業: 剣士
Lv: 1
HP: 150/150
EXP: 0/100

=== 冒険者の党の一斉攻撃! ===
勇者の強攻撃!威力:15
魔女の魔法攻撃!威力:20
騎士アーサーの強攻撃!威力:15
剣士ハヤテの強攻撃!威力:15

=== 職業固有の特殊行動 ===
勇者: 勇者の斬撃!威力:22.5
魔女: 魔女は魔法を唱えた!威力:40
MP: 40/50
騎士アーサー: 騎士アーサーは守護の構えをとった!
味方全員の被ダメージを15軽減!
剣士ハヤテ: 剣士ハヤテの二刀流攻撃!
右手: 15ダメージ!
左手: 15ダメージ!
合計: 30ダメージ!

=== パーティ全体回復 ===
勇者のHPが30回復!
魔女のHPが30回復!
騎士アーサーのHPが30回復!
剣士ハヤテのHPが30回復!

=== 経験値を配分 ===
150の経験値を獲得した!
レベルアップ!Lv.2になった!
攻撃力が上がった!
150の経験値を獲得した!
レベルアップ!Lv.2になった!
魔力が上がった!
150の経験値を獲得した!
レベルアップ!Lv.2になった!
攻撃力が上がった!
150の経験値を獲得した!
レベルアップ!Lv.2になった!
攻撃力が上がった!

【メンバー管理のテスト】
パーティが満員です
勇者がパーティを離れた
新人戦士がパーティに加わった!

=== 冒険者の党 ===
メンバー数: 4/4
戦士系: 3人
魔法使い系: 1人

7. 練習問題

基本問題

問題1: ポリモーフィズムの基本
次のコードの出力を予測してください。

Test.java
// **補足**
// 内部クラスとは「クラスの中にあるクラス」のこと (インナークラスと呼ばれることもある)
// Javaファイルに public クラスは1つだけというルールがある
// → ファイル名と public クラス名を一致させる必要がある
// → どれが「代表のクラス」かわからなくなるのを防ぐため
// → Test.java というファイルは Testクラスを public で作成する必要がある

/**
 * 動物クラス(内部クラス)
 */
class Animal {
    public void sound() {
        System.out.println("動物の鳴き声");
    }
}

/**
 * 犬クラス(内部クラス)
 */
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("ワンワン");
    }
}

/**
 * 猫クラス(内部クラス)
 */
class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("ニャー");
    }
}

/**
 * テストクラス
 */
public class Test {
    public static void main(String[] args) {
        Animal[] animals = {
            new Dog(),
            new Cat(),
            new Animal()
        };
        
        for (Animal a : animals) {
            a.sound();
        }
    }
}
解答を見る

出力:

ワンワン
ニャー
動物の鳴き声

理由:

  • animals[0]は実際はDogなので、Dogのsound()が実行
  • animals[1]は実際はCatなので、Catのsound()が実行
  • animals[2]はAnimalなので、Animalのsound()が実行
  • これが動的バインディング(ポリモーフィズム)

問題2: instanceof演算子
次のコードを完成させてください。

InstanceofPractice.java
public class InstanceofPractice {
    public static void action(Adventurer adv) {
        // advがWarriorなら"斬撃!"を表示
        // advがMageなら"魔法!"を表示
        // それ以外なら"通常攻撃"を表示
        
        // ここに実装
    }
    
    public static void main(String[] args) {
        action(new Warrior("勇者", 100, 20));
        action(new Mage("魔女", 80, 25));
        action(new Adventurer("冒険者", 100));
    }
}
解答例を見る
public static void action(Adventurer adv) {
    if (adv instanceof Warrior) {
        System.out.println("斬撃!");
    } else if (adv instanceof Mage) {
        System.out.println("魔法!");
    } else {
        System.out.println("通常攻撃");
    }
}

出力:

斬撃!
魔法!
通常攻撃

応用問題

問題3: アップキャストとダウンキャスト
次のコードの誤りを修正してください。

CastProblem.java
public class CastProblem {
    public static void main(String[] args) {
        Adventurer adv = new Mage("魔女", 80, 25);
        
        // 誤り1: Warriorにダウンキャスト
        Warrior warrior = (Warrior) adv;
        warrior.slash();
        
        // 誤り2: instanceofなしでダウンキャスト
        Mage mage = (Mage) adv;
        mage.cast();
    }
}
解答例を見る
public class CastProblem {
    public static void main(String[] args) {
        Adventurer adv = new Mage("魔女", 80, 25);
        
        // 修正1: instanceof で型確認してからダウンキャスト
        if (adv instanceof Warrior) {
            Warrior warrior = (Warrior) adv;
            warrior.slash();
        } else {
            System.out.println("Warriorではありません");
        }
        
        // 修正2: instanceof で確認してからキャスト
        if (adv instanceof Mage) {
            Mage mage = (Mage) adv;
            mage.cast();
        }
    }
}

問題4: パーティシステムの拡張
Partyクラスに次のメソッドを追加してください。

/**
 * 最もHPが低いメンバーを見つける
 * @return HPが最も低いメンバー(いない場合はnull)
 */
public Adventurer findLowestHpMember() {
    // ここに実装
}
解答例を見る
Party.java
    //--- ↓既存のメソッド群
    //--- 今まで追加したメソッド
    //--- ↑ 既存のメソッド群

    //--- ↓ 今回のメソッド追加

    /**
     * 最もHPが低いメンバーを見つける
     * @return HPが最も低いメンバー(いない場合はnull)
     */
	public Adventurer findLowestHpMember() {
		if (members.isEmpty()) {
			return null;
		}

		Adventurer lowestHpMember = members.get(0);

		for (Adventurer member : members) {
			if (member.getHp() < lowestHpMember.getHp()) {
				lowestHpMember = member;
			}
		}

		return lowestHpMember;
	}
PartyTest.java
// 使用例
public class PartyTest {

	public static void main(String[] args) {
		// パーティを作成
		Party party = new Party("冒険者の党", 4);

		// メンバーを追加(ポリモーフィズム)
		party.addMember(new Warrior("勇者"));
		party.addMember(new Mage("魔女"));
		party.addMember(new Knight("騎士アーサー"));
		party.addMember(new Swordsman("剣士ハヤテ"));
		System.out.println();

		Adventurer weakest = party.findLowestHpMember();
		if (weakest != null) {
			System.out.println("最もHPが低い: " + weakest.getName() + 
					" (HP: " + weakest.getHp() + ")");

		}
	}
}

実行結果

勇者がパーティに加わった!
魔女がパーティに加わった!
騎士アーサーがパーティに加わった!
剣士ハヤテがパーティに加わった!

最もHPが低い: 魔女 (HP: 80)

問題5: ポリモーフィズムの設計
次の要件を満たすメソッドを実装してください。

要件:

  • クラス名: PartyService
  • メソッド名: buffParty
  • 引数: Party party, String buffType
  • 戻り値: void
  • buffTypeに応じて、適切な職業にバフをかける
    • "physical": 戦士系の攻撃力を上げる
    • "magical": 魔法使い系の魔力を上げる
    • "all": 全員のHPを上げる
解答例を見る
PartyService.java
public class PartyService {
	public static void buffParty(Party party, String buffType) {
		System.out.println("=== " + buffType + " バフ発動! ===");

		for (Adventurer member : party.getMembers()) {
			switch (buffType) {
			case "physical":
				// 戦士系にバフ
				if (member instanceof Warrior) {
					Warrior w = (Warrior) member;
					int newPower = (int)(w.getPower() * 1.2);
					w.setPower(newPower);
					System.out.println(member.getName() + "の攻撃力UP!");
				}
				break;

			case "magical":
				// 魔法使い系にバフ
				if (member instanceof Mage) {
					Mage m = (Mage) member;
					int newMagic = (int)(m.getMagic() * 1.2);
					m.setMagic(newMagic);
					System.out.println(member.getName() + "の魔力UP!");
				}
				break;

			case "all":
				// 全員にバフ
				int newHp = (int)(member.getHp() * 1.1);
				member.setHp(newHp);
				System.out.println(member.getName() + "のHPUP!");
				break;

			default:
				System.out.println("不明なバフタイプ: " + buffType);
			}
		}
	}

    /**
     * 実行例
     */
	public static void main(String[] args) {

		// パーティを作成
		Party party = new Party("冒険者の党", 4);

		// メンバーを追加(ポリモーフィズム)
		party.addMember(new Warrior("勇者"));
		party.addMember(new Mage("魔女"));
		party.addMember(new Knight("騎士アーサー"));
		party.addMember(new Swordsman("剣士ハヤテ"));
		System.out.println();
		
		buffParty(party, "physical");  // 戦士系の攻撃力UP
		buffParty(party, "magical");   // 魔法使い系の魔力UP
		buffParty(party, "all");       // 全員のHPUP

	}
}

実行結果

勇者がパーティに加わった!
魔女がパーティに加わった!
騎士アーサーがパーティに加わった!
剣士ハヤテがパーティに加わった!

=== physical バフ発動! ===
勇者の攻撃力UP!
騎士アーサーの攻撃力UP!
剣士ハヤテの攻撃力UP!
=== magical バフ発動! ===
魔女の魔力UP!
=== all バフ発動! ===
勇者のHPUP!
魔女のHPUP!
騎士アーサーのHPUP!
剣士ハヤテのHPUP!

summary_banner.png

本記事では、ポリモーフィズムの基礎から実践的な使い方まで解説しました。

重要ポイント

AIはコードを書いてくれます
でも、そのコードが正しいかどうかを判断するのは人間 です
オブジェクト指向は書く技術ではなく、考え方を読む技術 です

1.ポリモーフィズムとは

  • 一つの型で多様なオブジェクトを扱える
  • コードをシンプルかつ柔軟にする
  • オブジェクト指向の三大要素の一つ

2.アップキャスト

  • 子→親への型変換
  • 自動的に行われる(安全)
  • 子クラス固有のメンバーは使えなくなる

3.ダウンキャスト

  • 親→子への型変換
  • 明示的なキャストが必要
  • 型が違うとClassCastException

4.instanceof演算子

  • 型を安全に確認
  • ダウンキャスト前に必須
  • nullは常にfalse

5.動的バインディング

  • 実行時に実際の型に基づいてメソッドが決定
  • ポリモーフィズムの核心

設計指針

  • 共通処理は親クラス型で扱う
  • 職業固有の処理はinstanceofで判定
  • 不要なダウンキャストは避ける
  • 拡張性を考慮した設計

reference_materials_banner-1.png

公式ドキュメント

書籍

  • 『Effective Java 第3版』Joshua Bloch著(継承とポリモーフィズムのベストプラクティス)
  • 『オブジェクト指向でなぜつくるのか』平澤章著(概念の理解に最適)
  • 『Java言語プログラミングレッスン』結城浩著(初心者向け丁寧な解説)

next_episode_preview_banner-1.png

次回は「第9回:抽象クラスとインターフェース」をお届けします!

  • 抽象クラスとは何か
  • abstract修飾子の使い方
  • テンプレートメソッドパターン
  • スキルシステムの実装

お楽しみに!


series_list_banner-1.png


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

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?