抽象クラスとポリモーフィズム

2025-08-01

はじめに

オブジェクト指向プログラミングの真髄である「抽象クラス」と「ポリモーフィズム」は、Javaプログラミングにおいて最も強力な概念の一つです。これらの概念を理解することで、柔軟で拡張性の高いプログラムを設計できるようになります。本記事では、具体例を交えながら、これらの概念を初学者にも分かりやすく解説します。

抽象クラスの基本

抽象クラスとは何か?

抽象クラスとは、インスタンス化できない不完全なクラスのことを指します。つまり、newキーワードを使って直接オブジェクトを作成することはできません。抽象クラスは、他のクラスに継承されることを前提に設計されます。

現実世界の例を例にしてみます。

  • 「動物」という概念は抽象クラスです。
  • 「犬」や「猫」は具体的なクラスです。
  • 私たちは「動物」そのものを見るのではなく、具体的な「犬」や「猫」を見ます。

抽象クラスの宣言方法

Animal クラスは、すべての動物に共通する基本情報(nameage)と共通動作(eat()sleep())を具象メソッドとして持ち、一方で動物ごとに異なる振る舞いである makeSound()(鳴く)と move()(動く)を抽象メソッドとして定義しています。

// abstractキーワードを使用して抽象クラスを宣言
abstract class Animal {
    protected String name;
    protected int age;

    // コンストラクタ
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 具象メソッド - 具体的な実装を持つ
    public void eat() {
        System.out.println(name + "が食事をしています。");
    }

    public void sleep() {
        System.out.println(name + "が眠っています。");
    }

    // 抽象メソッド - 実装を持たない
    public abstract void makeSound();
    public abstract void move();
}

つまり、このクラスは「動物の共通部分をまとめ、個別の特徴はサブクラスで実装させる」というテンプレート的な設計の基盤を提供するためのクラスです。

抽象メソッドの特徴

抽象メソッドには以下の特徴があります。

  • メソッド本体がない({}とその中の実装がない)
  • abstractキーワードで修飾される
  • サブクラスで必ずオーバーライドして実装する必要がある
  • 抽象メソッドを持つクラスは必ず抽象クラスでなければならない

抽象クラスの実装

抽象クラスを継承する具体クラス

抽象クラスを継承する具体クラスでは、すべての抽象メソッドを実装する必要があります。抽象クラスAnimalを継承した3つの具象クラス(DogBirdFish)を定義しています。

/**
 * 犬クラス - Animal抽象クラスを継承
 */
class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);  // 親クラスのコンストラクタを呼び出し
        this.breed = breed;
    }

    // 抽象メソッドの実装(オーバーライド)
    @Override
    public void makeSound() {
        System.out.println(name + "が「ワンワン!」と吠えています。");
    }

    @Override
    public void move() {
        System.out.println(name + "が走り回っています。");
    }

    // 犬クラス独自のメソッド
    public void fetch() {
        System.out.println(name + "がボールを取ってきました!");
    }
}

/**
 * 鳥クラス - Animal抽象クラスを継承
 */
class Bird extends Animal {
    private double wingSpan;
    private boolean canFly;

    public Bird(String name, int age, double wingSpan, boolean canFly) {
        super(name, age);
        this.wingSpan = wingSpan;
        this.canFly = canFly;
    }

    // 抽象メソッドの実装(オーバーライド)
    @Override
    public void makeSound() {
        System.out.println(name + "が「チュンチュン」と鳴いています。");
    }

    @Override
    public void move() {
        if (canFly) {
            System.out.println(name + "が空を飛んでいます!翼幅: " + wingSpan + "cm");
        } else {
            System.out.println(name + "が地面を歩いています。");
        }
    }

    // 鳥クラス独自のメソッド
    public void buildNest() {
        System.out.println(name + "が巣を作っています。");
    }
}

/**
 * 魚クラス - Animal抽象クラスを継承
 */
class Fish extends Animal {
    private String waterType;

    public Fish(String name, int age, String waterType) {
        super(name, age);
        this.waterType = waterType;
    }

    // 抽象メソッドの実装(オーバーライド)
    @Override
    public void makeSound() {
        System.out.println(name + "が「ブクブク」と音を立てています。");
    }

    @Override
    public void move() {
        System.out.println(name + "が" + waterType + "を泳いでいます。");
    }

    // 魚クラス独自のメソッド
    public void swim() {
        System.out.println(name + "が優雅に泳いでいます。");
    }
}

それぞれがmakeSound()move()という抽象メソッドをオーバーライドして動物ごとの動作や鳴き声を具体的に実装し、さらに各クラス独自の振る舞い(犬のfetch()、鳥のbuildNest()、魚のswim())を追加しています。

つまり、「共通の基本構造を持ちながら、各動物の特徴的な行動を表現する」オブジェクト指向の**継承と多態性(ポリモーフィズム)**の実例です。

抽象クラスの利点

抽象クラスは、オブジェクト指向プログラミングにおいて「共通の性質を持つが、単独では具体的な動作を持たないクラス」を定義するために存在します。具体的なインスタンスを生成するのではなく、共通の基本構造やルールを定める設計図としての役割を果たします。

抽象クラスを使用する主な利点は、共通機能の提供によって具象メソッドで共通の処理をまとめられ、子クラスで重複コードを書く必要がないこと、契約の強制により抽象メソッドを通して子クラスが実装すべきメソッドを明確にできること、設計のガイドラインとしてクラス階層全体の構造を整理できること、そして柔軟性の面で将来の拡張を見据えた堅牢な設計が可能になることです。

つまり、抽象クラスは「共通部分の再利用」と「派生クラスに対する設計上の指針提供」の両立を実現する、ソフトウェア設計における重要な要素です。

ポリモーフィズム

ポリモーフィズムとは?

ポリモーフィズム(多態性)とは、同じインターフェースでありながら、異なる動作をする能力を指します。簡単に言えば、「同じメッセージを送っても、受け取るオブジェクトによって異なる反応を示す」ことです。

現実世界の例を以下に示します。

  • 「描く」という命令に対して
  • 画家は絵を描く
  • プログラマーはコードを描く(書く)
  • 建築家は設計図を描く
  • 同じ「描く」という動作でも、対象によって結果が異なる

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

このコードは、**ポリモーフィズム(多態性)**の基本的な例を示しています。

public class PolymorphismBasicExample {
    public static void main(String[] args) {
        // 親クラス型の変数に子クラスのオブジェクトを代入
        Animal myDog = new Dog("ポチ", 3, "柴犬");
        Animal myBird = new Bird("ピヨ", 2, 25.5, true);
        Animal myFish = new Fish("金太郎", 1, "淡水");

        // 同じメソッド呼び出しで異なる動作
        myDog.makeSound();  // 犬の鳴き声
        myBird.makeSound(); // 鳥の鳴き声
        myFish.makeSound(); // 魚の音
    }
}

Animal型の変数(親クラス型)に、DogBirdFishといった子クラスのインスタンスを代入し、同じメソッドmakeSound()を呼び出しています。しかし実行時には、それぞれのクラスでオーバーライドされたメソッドが呼ばれ、犬・鳥・魚それぞれ固有の鳴き声(動作)が出力されます。

つまりこのプログラムは、**「同じ命令(makeSound)でも、実際のオブジェクトの型によって異なる結果を生む」**というポリモーフィズムの性質を明確に表しています。

ポリモーフィズムの実践

コレクションを利用したポリモーフィズム

ポリモーフィズムの真価は、コレクションと組み合わさったときに発揮されます。

Animalを継承したDogBirdFishのインスタンスを共通のAnimal型リストで管理し、同じメソッド(eat()makeSound()move())を呼び出しても、実際には各クラスごとに異なる動作が実行されます。これにより、1つのインターフェースで多様な動作を実現できるというポリモーフィズムの特徴が確認できます。

さらに後半では、instanceofと型キャストを使ってDog型だけを取り出し、fetch()という犬クラス特有のメソッドを実行しています。つまりこのプログラムは、「共通の親クラスを介して異なるサブクラスを一括で扱いつつ、必要に応じて個別の機能にもアクセスできる」というオブジェクト指向の柔軟な設計を具体的に示しています。

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

/**
 * ポリモーフィズムの実践例
 */
public class PolymorphismPractice {
    public static void main(String[] args) {
        // Animal型のリストを作成
        List animals = new ArrayList<>();

        // 様々な動物を追加
        animals.add(new Dog("ポチ", 3, "柴犬"));
        animals.add(new Bird("スズキ", 1, 15.0, true));
        animals.add(new Fish("コイ之助", 2, "淡水"));
        animals.add(new Dog("ハチ", 5, "秋田犬"));
        animals.add(new Bird("ペンギン", 4, 10.0, false));

        System.out.println("=== 動物園の様子 ===");

        // すべての動物に同じメッセージを送る
        for (Animal animal : animals) {
            animal.eat();       // 同じメソッド呼び出し
            animal.makeSound(); // しかし異なる動作
            animal.move();      // 各動物に適した動作
            System.out.println("---");
        }

        // 特定の型の処理
        System.out.println("=== 犬だけを処理 ===");
        for (Animal animal : animals) {
            if (animal instanceof Dog) {
                Dog dog = (Dog) animal;  // 型キャスト
                dog.fetch();  // Dogクラス独自のメソッド
            }
        }
    }
}

メソッドパラメータでのポリモーフィズム

ポリモーフィズムはメソッドのパラメータでも活用できます。

ZooクラスはAnimal型のリストを持ち、DogBirdFishといった異なる動物オブジェクトを共通の型(Animal)として管理しています。addAnimal()メソッドでさまざまな動物を追加し、feedAllAnimals()performShow()では全動物に対して共通メソッド(eat()makeSound()move())を呼び出すことで、同じコードで異なる振る舞いを実現しています。

さらにperformBirdShow()ではinstanceofを使ってBird型のみに絞り込み、buildNest()といった鳥クラス特有のメソッドを実行しています。

/**
 * 動物園クラス
 */
class Zoo {
    private List animals = new ArrayList<>();

    // Animal型のパラメータを受け取る - ポリモーフィズムを活用
    public void addAnimal(Animal animal) {
        animals.add(animal);
        System.out.println(animal.name + "を動物園に追加しました!");
    }

    // すべての動物にエサを与える
    public void feedAllAnimals() {
        System.out.println("=== エサやり時間 ===");
        for (Animal animal : animals) {
            animal.eat();
        }
    }

    // 動物たちのパフォーマンスショー
    public void performShow() {
        System.out.println("=== パフォーマンスショー ===");
        for (Animal animal : animals) {
            System.out.println(animal.name + "の出番です!");
            animal.makeSound();
            animal.move();
            System.out.println("拍手!👏");
            System.out.println("---");
        }
    }

    // 特定の種類の動物だけを処理
    public void performBirdShow() {
        System.out.println("=== 鳥のショー ===");
        for (Animal animal : animals) {
            if (animal instanceof Bird) {
                Bird bird = (Bird) animal;
                bird.makeSound();
                bird.move();
                bird.buildNest();
                System.out.println("---");
            }
        }
    }
}

// 使用例
public class ZooExample {
    public static void main(String[] args) {
        Zoo zoo = new Zoo();

        // 様々な動物を追加
        zoo.addAnimal(new Dog("シロ", 2, "ホワイトシェパード"));
        zoo.addAnimal(new Bird("オウム", 3, 40.0, true));
        zoo.addAnimal(new Fish("ニモ", 1, "海水"));
        zoo.addAnimal(new Bird("ペンギン", 4, 12.0, false));

        // ポリモーフィズムを活用したメソッド呼び出し
        zoo.feedAllAnimals();
        zoo.performShow();
        zoo.performBirdShow();
    }
}

つまりこのプログラムは、ポリモーフィズムにより「共通の親クラスを通じて多様なサブクラスを一括管理し、必要に応じて個別の機能も呼び出せる」ことを示す、実践的なオブジェクト指向設計の例です。

ポリモーフィズムとの連携

継承とオーバーライドの真価は、ポリモーフィズム(多態性)と組み合わさったときに発揮されます。このコードは、**継承とポリモーフィズム(多態性)**の仕組みを実際に示すデモプログラムです。
Shape配列にはCircleRectangleSquareなど異なる図形オブジェクトを格納し、共通のcalculateAreatoStringを呼び出すことで、同じメソッド名でもクラスごとに異なる処理が実行されます。
同様にAdvancedAnimal配列には犬・猫・鳥・魚をまとめ、makeSoundmoveを呼ぶとそれぞれ固有の動作を行います。

/**
 * 継承とポリモーフィズムのデモンストレーション
 */
public class InheritanceDemo {
    public static void main(String[] args) {
        // 様々な図形オブジェクトを作成
        Shape[] shapes = {
            new Circle("赤", true, 5.0),
            new Rectangle("青", false, 4.0, 6.0),
            new Square("緑", true, 3.0),
            new Circle("黄", false, 2.5)
        };

        // ポリモーフィズムの力:同じメソッド呼び出しで異なる動作
        System.out.println("=== 各図形の情報 ===");
        for (Shape shape : shapes) {
            System.out.println(shape.toString());
            System.out.printf("面積: %.2f%n", shape.calculateArea());
            System.out.printf("周長: %.2f%n", shape.calculatePerimeter());
            System.out.println("---");
        }

        // 動物の例
        AdvancedAnimal[] animals = {
            new Dog("ポチ", 3, "柴犬"),
            new Cat("タマ", 2, true),
            new Bird("ピヨ", 30.5),
            new Fish("金太郎", "淡水")
        };

        System.out.println("=== 各動物の動作 ===");
        for (AdvancedAnimal animal : animals) {
            animal.makeSound(); // 各動物の鳴き声
            animal.move();      // 各動物の移動方法
            System.out.println("---");
        }
    }
}

これにより、共通インターフェースで多様な振る舞いを実現するポリモーフィズムの利点が理解できます。

より現実的な例 – 図形計算アプリケーション

抽象クラスを使用した図形階層

Shape クラスは「色(color)」と「塗りつぶし状態(filled)」を共通フィールドとして持ち、calculateArea()(面積)と calculatePerimeter()(周長)という抽象メソッドを定義しています。これにより、すべてのサブクラスは自分に適した面積・周長の計算方法を必ず実装しなければなりません。

サブクラスとして、Circle(円)、Rectangle(四角形)、Triangle(三角形)がそれぞれ定義されており、これらは Shape の抽象メソッドをオーバーライドして独自の計算処理を実装しています。また、各クラスは displayInfo() メソッドを上書きして、図形の詳細(半径・幅・高さなど)や計算結果を出力するようになっています。

/**
 * 図形の抽象クラス
 */
abstract class Shape {
    protected String color;
    protected boolean filled;

    public Shape(String color, boolean filled) {
        this.color = color;
        this.filled = filled;
    }

    // 抽象メソッド - 子クラスで実装必須
    public abstract double calculateArea();
    public abstract double calculatePerimeter();

    // 具象メソッド
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public boolean isFilled() {
        return filled;
    }

    public void setFilled(boolean filled) {
        this.filled = filled;
    }

    // 具象メソッドだがオーバーライド可能
    public void displayInfo() {
        System.out.println("図形: 色=" + color + ", 塗りつぶし=" + filled);
    }
}

/**
 * 円クラス
 */
class Circle extends Shape {
    private double radius;

    public Circle(String color, boolean filled, double radius) {
        super(color, filled);
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }

    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.printf("円 - 半径: %.2f, 面積: %.2f, 円周: %.2f%n", 
                         radius, calculateArea(), calculatePerimeter());
    }

    public double getRadius() {
        return radius;
    }
}

/**
 * 四角形クラス
 */
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(String color, boolean filled, double width, double height) {
        super(color, filled);
        this.width = width;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return width * height;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * (width + height);
    }

    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.printf("四角形 - 幅: %.2f, 高さ: %.2f, 面積: %.2f, 周長: %.2f%n", 
                         width, height, calculateArea(), calculatePerimeter());
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }
}

/**
 * 三角形クラス
 */
class Triangle extends Shape {
    private double base;
    private double height;
    private double side1;
    private double side2;

    public Triangle(String color, boolean filled, double base, double height, double side1, double side2) {
        super(color, filled);
        this.base = base;
        this.height = height;
        this.side1 = side1;
        this.side2 = side2;
    }

    @Override
    public double calculateArea() {
        return 0.5 * base * height;
    }

    @Override
    public double calculatePerimeter() {
        return base + side1 + side2;
    }

    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.printf("三角形 - 底辺: %.2f, 高さ: %.2f, 面積: %.2f, 周長: %.2f%n", 
                         base, height, calculateArea(), calculatePerimeter());
    }
}

この設計によって、異なる図形でも同じインターフェース(Shape)を通して統一的に扱えるため、共通化・拡張性・保守性に優れたポリモーフィズムの活用例となっています。

図形管理システム

ShapeManager クラスは、Shape(図形の基底クラス)を継承した複数の具象クラス(CircleRectangleTriangleなど)を共通のShape型として管理しています。図形の追加・一覧表示・総面積や総周長の計算などをすべて共通のインターフェースで実行できるのが特徴です。

各図形クラスは、calculateArea()(面積計算)やcalculatePerimeter()(周長計算)といったメソッドを独自にオーバーライドしており、ShapeManager はそれらをポリモーフィズムを通じて呼び出すことで、図形の種類を意識せずに統一的な処理を実現しています。

また、displayShapesByColor()メソッドでは、図形の色で絞り込みを行い、条件に合う図形だけを表示しています。

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

/**
 * 図形管理システム
 */
class ShapeManager {
    private List shapes = new ArrayList<>();

    // 図形を追加
    public void addShape(Shape shape) {
        shapes.add(shape);
        System.out.println("図形を追加しました");
        shape.displayInfo();
    }

    // 総面積を計算
    public double calculateTotalArea() {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.calculateArea();
        }
        return totalArea;
    }

    // 総周長を計算
    public double calculateTotalPerimeter() {
        double totalPerimeter = 0;
        for (Shape shape : shapes) {
            totalPerimeter += shape.calculatePerimeter();
        }
        return totalPerimeter;
    }

    // すべての図形情報を表示
    public void displayAllShapes() {
        System.out.println("=== すべての図形情報 ===");
        for (Shape shape : shapes) {
            shape.displayInfo();
        }
    }

    // 特定の色の図形だけを表示
    public void displayShapesByColor(String color) {
        System.out.println("=== 色が「" + color + "」の図形 ===");
        for (Shape shape : shapes) {
            if (shape.getColor().equalsIgnoreCase(color)) {
                shape.displayInfo();
            }
        }
    }
}

// 使用例
public class ShapeSystemExample {
    public static void main(String[] args) {
        ShapeManager manager = new ShapeManager();

        // 様々な図形を追加
        manager.addShape(new Circle("赤", true, 5.0));
        manager.addShape(new Rectangle("青", false, 4.0, 6.0));
        manager.addShape(new Triangle("緑", true, 3.0, 4.0, 5.0, 4.0));
        manager.addShape(new Circle("黄", true, 3.0));
        manager.addShape(new Rectangle("赤", true, 2.0, 2.0));

        System.out.println();

        // ポリモーフィズムを活用した計算
        System.out.printf("総面積: %.2f%n", manager.calculateTotalArea());
        System.out.printf("総周長: %.2f%n", manager.calculateTotalPerimeter());

        System.out.println();

        // すべての図形を表示
        manager.displayAllShapes();

        System.out.println();

        // 特定の色の図形を表示
        manager.displayShapesByColor("赤");
    }
}

つまりこのプログラムは、「共通の抽象クラスを軸に異なる図形を統一的に扱い、拡張性・再利用性・柔軟性を高めた設計」を実践したオブジェクト指向の好例です。

抽象クラスとポリモーフィズムの組み合わせ

設計上のメリット

抽象クラスとポリモーフィズムを組み合わせることで、共通のロジックを抽象クラスに集中させて保守性を高めつつ、新しい具象クラスを追加しても既存コードを変更する必要がない拡張性を確保できる。また、実行時には異なる振る舞いを動的に選択できる柔軟性を持ち、さらにコンパイル時の型チェックによって型安全性も維持されるため、堅牢で拡張可能なプログラム設計が可能になる。

実際の設計例

次のコードは、抽象クラスとポリモーフィズムを利用して支払い処理を統一的に扱う設計の例です。

PaymentMethod は「支払い方法」を表す抽象クラスで、共通のフィールド(名義人・残高)と共通処理(残高確認など)を持ちながら、pay()getPaymentDetails() という抽象メソッドで「支払いの実行」と「支払い情報の表示」をサブクラスに実装を委ねています

CreditCard クラスと ElectronicMoney クラスはそれぞれ異なる支払い手段を表し、PaymentMethod の抽象メソッドをオーバーライドして自分に合った支払いロジックを実装しています。

PaymentProcessor クラスは、PaymentMethod 型(親クラス型)を引数に取り、どの支払い手段であっても同じインターフェースで支払い処理を行えるように設計されています。

/**
 * 支払い方法の抽象クラス
 */
abstract class PaymentMethod {
    protected String holderName;
    protected double balance;

    public PaymentMethod(String holderName, double balance) {
        this.holderName = holderName;
        this.balance = balance;
    }

    // 抽象メソッド
    public abstract boolean pay(double amount);
    public abstract String getPaymentDetails();

    // 具象メソッド
    public double getBalance() {
        return balance;
    }

    public String getHolderName() {
        return holderName;
    }

    protected boolean canPay(double amount) {
        return balance >= amount;
    }
}

/**
 * クレジットカードクラス
 */
class CreditCard extends PaymentMethod {
    private String cardNumber;
    private String expiryDate;

    public CreditCard(String holderName, double balance, String cardNumber, String expiryDate) {
        super(holderName, balance);
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
    }

    @Override
    public boolean pay(double amount) {
        if (canPay(amount)) {
            balance -= amount;
            System.out.printf("クレジットカードで %.2f円支払いました%n", amount);
            return true;
        } else {
            System.out.println("残高が不足しています");
            return false;
        }
    }

    @Override
    public String getPaymentDetails() {
        return String.format("クレジットカード: %s (%s) 残高: %.2f円", 
                           holderName, maskCardNumber(cardNumber), balance);
    }

    private String maskCardNumber(String cardNumber) {
        return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
    }
}

/**
 * 電子マネークラス
 */
class ElectronicMoney extends PaymentMethod {
    private String emoneyId;

    public ElectronicMoney(String holderName, double balance, String emoneyId) {
        super(holderName, balance);
        this.emoneyId = emoneyId;
    }

    @Override
    public boolean pay(double amount) {
        if (canPay(amount)) {
            balance -= amount;
            System.out.printf("電子マネーで %.2f円支払いました%n", amount);
            return true;
        } else {
            System.out.println("残高が不足しています");
            return false;
        }
    }

    @Override
    public String getPaymentDetails() {
        return String.format("電子マネー: %s (ID: %s) 残高: %.2f円", 
                           holderName, emoneyId, balance);
    }
}

/**
 * 支払い処理クラス
 */
class PaymentProcessor {
    public void processPayment(PaymentMethod payment, double amount) {
        System.out.println("支払いを処理します...");
        System.out.println(payment.getPaymentDetails());

        if (payment.pay(amount)) {
            System.out.println("支払いが成功しました");
        } else {
            System.out.println("支払いに失敗しました");
        }
        System.out.println("---");
    }
}

// 使用例
public class PaymentExample {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();

        // 様々な支払い方法
        PaymentMethod[] payments = {
            new CreditCard("山田太郎", 50000, "1234567812345678", "12/25"),
            new ElectronicMoney("鈴木花子", 3000, "EM00123456"),
            new CreditCard("佐藤健太", 100000, "8765432187654321", "06/26")
        };

        // 同じ処理インターフェースで異なる支払い方法を処理
        for (PaymentMethod payment : payments) {
            processor.processPayment(payment, 2500);
        }
    }
}

つまりこのプログラムは、抽象クラスで共通部分を定義しつつ、ポリモーフィズムによって異なる支払い方法を統一的に扱うという、現実的で拡張性の高いオブジェクト指向設計を実践的に示したものです。

まとめ

抽象クラスとポリモーフィズムを理解することで、共通機能を抽象クラスにまとめてコードの再利用性を高め、新しいクラスを容易に追加できる拡張性を確保し、ロジックを一箇所に集中させて保守性を向上させるとともに、実行時に振る舞いを動的に変更できる柔軟な設計が可能になります。

初学者にとっては概念的に難しい部分もありますが、実際にコードを書いて試してみることで、その威力を実感できるでしょう。まずは簡単な例から始めて、徐々に複雑なシステムに応用していくことをお勧めします。

演習問題

初級問題(3問)

初級問題1: 基本的な抽象クラスの作成

以下の要件を満たすプログラムを作成してください。

要件:

  1. Vehicle(乗り物)という抽象クラスを作成する
  2. 以下の抽象メソッドを持つ:
  • start(): 乗り物を発進させる
  • stop(): 乗り物を停止させる
  1. 以下の具象メソッドを持つ:
  • getFuelLevel(): 燃料レベルを返す(常に100を返す)
  1. CarクラスとBicycleクラスを作成し、Vehicleを継承する
  2. 各クラスで抽象メソッドを適切に実装する

初級問題2: オーバーライドの基本

以下のプログラムを完成させ、実行結果が以下のようになるようにしてください。

実行結果:

犬がワンワンと吠えます
猫がニャーと鳴きます
犬が走り回ります
猫がそっと歩きます
abstract class Animal {
    public abstract void makeSound();
    public void move() {
        System.out.println("動物が移動します");
    }
}

class Dog extends Animal {
    // ここにコードを記述
}

class Cat extends Animal {
    // ここにコードを記述
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound();
        cat.makeSound();
        dog.move();
        cat.move();
    }
}

初級問題3: コンストラクタと継承

以下のプログラムの実行結果を答え、その理由を説明してください。

abstract class Base {
    public Base() {
        System.out.println("Baseコンストラクタ");
        init();
    }

    protected abstract void init();
}

class Derived extends Base {
    private String name = "Java";

    public Derived() {
        System.out.println("Derivedコンストラクタ");
    }

    @Override
    protected void init() {
        System.out.println("初期化: " + name);
    }
}

public class Main {
    public static void main(String[] args) {
        new Derived();
    }
}

中級問題(6問)

中級問題1: 図形クラスの設計

以下の要件を満たす図形クラスの階層を設計してください。

要件:

  1. Shape抽象クラスを作成
  • 抽象メソッド: calculateArea()(面積計算), calculatePerimeter()(周長計算)
  • 具象メソッド: getColor(), setColor()
  1. Circle, Rectangle, Triangleクラスを実装
  2. 各図形クラスは適切にコンストラクタとメソッドを実装
  3. メインメソッドで各図形の面積と周長を表示

中級問題2: 銀行口座システム

以下の銀行口座クラスの設計において、__1____5__に入る適切なコードを記入してください。

abstract class BankAccount {
    protected String accountNumber;
    protected double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    // __1__ void deposit(double amount);

    public __2__ withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    public double getBalance() {
        return balance;
    }

    public abstract void applyMonthlyFee();
}

class SavingsAccount __3__ BankAccount {
    private static final double INTEREST_RATE = 0.01;

    public SavingsAccount(String accountNumber, double initialBalance) {
        __4__(accountNumber, initialBalance);
    }

    @Override
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println(amount + "円預け入れました");
        }
    }

    @Override
    public __5__ applyMonthlyFee() {
        double interest = balance * INTEREST_RATE;
        balance += interest;
        System.out.println("利息 " + interest + "円が付与されました");
    }
}

中級問題3: 従業員管理システム

以下のコードの間違いを修正し、正しく動作するようにしてください。

abstract class Employee {
    private String name;
    protected double baseSalary;

    public Employee(String name, double baseSalary) {
        this.name = name;
        this.baseSalary = baseSalary;
    }

    public abstract double calculateSalary();

    public void displayInfo() {
        System.out.println("名前: " + name);
        System.out.println("基本給: " + baseSalary);
    }
}

class FullTimeEmployee extends Employee {
    private double bonus;

    public FullTimeEmployee(String name, double baseSalary, double bonus) {
        super(name, baseSalary);
        this.bonus = bonus;
    }

    public double calculateSalary() {
        return baseSalary + bonus;
    }
}

class PartTimeEmployee extends Employee {
    private int hoursWorked;
    private double hourlyRate;

    public PartTimeEmployee(String name, int hoursWorked, double hourlyRate) {
        super(name, 0);
        this.hoursWorked = hoursWorked;
        this.hourlyRate = hourlyRate;
    }

    public double calculateSalary() {
        baseSalary = hoursWorked * hourlyRate;
        return baseSalary;
    }
}

中級問題4: スーパークラスのメソッド呼び出し

以下のプログラムを完成させ、実行結果が以下のようになるようにしてください。

実行結果:

電子機器の電源を入れます
スマートフォンの起動画面を表示
スマートフォンの電源を切ります
電子機器の電源を切ります
abstract class ElectronicDevice {
    public void turnOn() {
        System.out.println("電子機器の電源を入れます");
    }

    public void turnOff() {
        System.out.println("電子機器の電源を切ります");
    }

    public abstract void displayStartupScreen();
}

class SmartPhone extends ElectronicDevice {
    // ここにコードを記述

    public void specialShutdown() {
        System.out.println("スマートフォンの電源を切ります");
        // 親クラスのturnOffメソッドを呼び出す
    }
}

public class Main {
    public static void main(String[] args) {
        SmartPhone phone = new SmartPhone();
        phone.turnOn();
        phone.displayStartupScreen();
        phone.specialShutdown();
    }
}

中級問題5: ポリモーフィズムの理解

以下のプログラムの出力結果を答え、その理由を説明してください。

abstract class Animal {
    public void eat() {
        System.out.println("動物が食事をしています");
    }

    public abstract void makeSound();
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("ワンワン");
    }

    public void fetch() {
        System.out.println("ボールを取ってきます");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("ニャー");
    }

    public void climb() {
        System.out.println("木に登ります");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animals = new Animal[2];
        animals[0] = new Dog();
        animals[1] = new Cat();

        for (Animal animal : animals) {
            animal.eat();
            animal.makeSound();

            if (animal instanceof Dog) {
                Dog dog = (Dog) animal;
                dog.fetch();
            }
        }
    }
}

中級問題6: フィールドの隠蔽とメソッドオーバーライド

以下のプログラムの出力結果を予想し、その理由を説明してください。

abstract class Parent {
    protected String value = "親の値";

    public void printValue() {
        System.out.println(value);
    }

    public abstract void setValue();
}

class Child extends Parent {
    protected String value = "子の値";

    @Override
    public void printValue() {
        System.out.println("Child: " + value);
    }

    @Override
    public void setValue() {
        value = "変更された子の値";
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child();
        obj.printValue();
        obj.setValue();
        obj.printValue();

        System.out.println(((Child) obj).value);
    }
}

上級問題(3問)

上級問題1: テンプレートメソッドパターン

飲み物を作る過程を表す抽象クラスを作成してください。

要件:

  1. Beverage抽象クラスを作成
  2. テンプレートメソッド prepareRecipe() で以下の順序を固定:
  • お湯を沸かす
  • brew()(抽象メソッド)で飲み物を淹れる
  • カップに注ぐ
  • addCondiments()(抽象メソッド)で調味料を追加
  1. CoffeeTeaクラスで具象化
  2. 実行結果が以下のようになるように実装

実行結果:

=== コーヒーを準備 ===
お湯を沸かします
コーヒーをドリップします
カップに注ぎます
砂糖とミルクを追加します

=== お茶を準備 ===
お湯を沸かします
紅茶を浸します
カップに注ぎます
レモンを追加します

上級問題2: 状態管理システム

以下の要件を満たす文書状態管理システムを作成してください。

要件:

  1. DocumentState抽象クラスを作成
  • 抽象メソッド: publish(), archive(), draft()
  • 具象メソッド: getStateName()
  1. DraftState, PublishedState, ArchivedStateクラスを実装
  2. Documentクラスで状態を管理
  3. 状態遷移のルール:
  • 下書き → 公開 → アーカイブ
  • アーカイブ → 下書き(可能)
  • 公開 → 下書き(不可)

上級問題3: 複雑な継承階層

以下の要件を満たすゲームのキャラクターシステムを設計してください。

要件:

  1. GameCharacter抽象クラス(HP, MP, 攻撃力などの基本属性)
  2. PlayableCharacter抽象クラス(GameCharacterを継承、プレイヤー操作可能な機能)
  3. NPC抽象クラス(GameCharacterを継承、AI制御の機能)
  4. 具象クラス:Warrior, Mage, Merchant, Enemy
  5. 各クラスで適切なメソッドをオーバーライド
  6. ポリモーフィズムを活用したバトルシステム

演習問題 解答例

初級問題1 解答例

abstract class Vehicle {
    public abstract void start();
    public abstract void stop();

    public int getFuelLevel() {
        return 100;
    }
}

class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("車のエンジンを始動します");
    }

    @Override
    public void stop() {
        System.out.println("車のエンジンを停止します");
    }
}

class Bicycle extends Vehicle {
    @Override
    public void start() {
        System.out.println("自転車に乗り始めます");
    }

    @Override
    public void stop() {
        System.out.println("自転車を止めます");
    }
}

初級問題2 解答例

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("犬がワンワンと吠えます");
    }

    @Override
    public void move() {
        System.out.println("犬が走り回ります");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("猫がニャーと鳴きます");
    }

    @Override
    public void move() {
        System.out.println("猫がそっと歩きます");
    }
}

初級問題3 解答例

出力結果:

Baseコンストラクタ
初期化: null
Derivedコンストラクタ

理由:
親クラスのコンストラクタが先に実行され、その中でinit()メソッドが呼び出されます。この時点では子クラスのフィールドnameはまだ初期化されていないため(コンストラクタの実行前)、nullが表示されます。

中級問題 解答例

中級問題1: 図形クラスの設計 解答例

abstract class Shape {
    private String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象メソッド
    public abstract double calculateArea();
    public abstract double calculatePerimeter();

    // 具象メソッド
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    // 図形情報を表示するメソッド
    public void displayInfo() {
        System.out.println("図形の色: " + color);
        System.out.printf("面積: %.2f%n", calculateArea());
        System.out.printf("周長: %.2f%n", calculatePerimeter());
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }

    @Override
    public void displayInfo() {
        System.out.println("=== 円 ===");
        super.displayInfo();
        System.out.printf("半径: %.2f%n", radius);
        System.out.println();
    }
}

class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return width * height;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * (width + height);
    }

    @Override
    public void displayInfo() {
        System.out.println("=== 四角形 ===");
        super.displayInfo();
        System.out.printf("幅: %.2f, 高さ: %.2f%n", width, height);
        System.out.println();
    }
}

class Triangle extends Shape {
    private double base;
    private double height;
    private double side1;
    private double side2;

    public Triangle(String color, double base, double height, double side1, double side2) {
        super(color);
        this.base = base;
        this.height = height;
        this.side1 = side1;
        this.side2 = side2;
    }

    @Override
    public double calculateArea() {
        return 0.5 * base * height;
    }

    @Override
    public double calculatePerimeter() {
        return base + side1 + side2;
    }

    @Override
    public void displayInfo() {
        System.out.println("=== 三角形 ===");
        super.displayInfo();
        System.out.printf("底辺: %.2f, 高さ: %.2f%n", base, height);
        System.out.printf("辺1: %.2f, 辺2: %.2f%n", side1, side2);
        System.out.println();
    }
}

public class ShapeTest {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Circle("赤", 5.0),
            new Rectangle("青", 4.0, 6.0),
            new Triangle("緑", 3.0, 4.0, 5.0, 4.0)
        };

        for (Shape shape : shapes) {
            shape.displayInfo();
        }
    }
}

中級問題2: 銀行口座システム 解答例

abstract class BankAccount {
    protected String accountNumber;
    protected double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    // __1__ abstract void deposit(double amount);
    public abstract void deposit(double amount);

    // __2__ public boolean withdraw(double amount) {
    public boolean withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    public double getBalance() {
        return balance;
    }

    public abstract void applyMonthlyFee();
}

// __3__ class SavingsAccount extends BankAccount {
class SavingsAccount extends BankAccount {
    private static final double INTEREST_RATE = 0.01;

    public SavingsAccount(String accountNumber, double initialBalance) {
        // __4__ super(accountNumber, initialBalance);
        super(accountNumber, initialBalance);
    }

    @Override
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println(amount + "円預け入れました");
        }
    }

    @Override
    // __5__ public void applyMonthlyFee() {
    public void applyMonthlyFee() {
        double interest = balance * INTEREST_RATE;
        balance += interest;
        System.out.println("利息 " + interest + "円が付与されました");
    }
}

中級問題3: 従業員管理システム 解答例

abstract class Employee {
    private String name;
    protected double baseSalary;

    public Employee(String name, double baseSalary) {
        this.name = name;
        this.baseSalary = baseSalary;
    }

    public abstract double calculateSalary();

    public void displayInfo() {
        System.out.println("名前: " + name);
        System.out.println("基本給: " + baseSalary);
    }

    // ゲッターを追加
    public String getName() {
        return name;
    }
}

class FullTimeEmployee extends Employee {
    private double bonus;

    public FullTimeEmployee(String name, double baseSalary, double bonus) {
        super(name, baseSalary);
        this.bonus = bonus;
    }

    @Override
    public double calculateSalary() {
        return baseSalary + bonus;
    }

    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.println("ボーナス: " + bonus);
        System.out.println("総支給額: " + calculateSalary());
    }
}

class PartTimeEmployee extends Employee {
    private int hoursWorked;
    private double hourlyRate;

    // 修正: 基本給を0で初期化し、時給と労働時間で計算
    public PartTimeEmployee(String name, int hoursWorked, double hourlyRate) {
        super(name, 0); // 基本給は0
        this.hoursWorked = hoursWorked;
        this.hourlyRate = hourlyRate;
    }

    @Override
    public double calculateSalary() {
        // baseSalaryを直接変更するのではなく、計算結果を返す
        return hoursWorked * hourlyRate;
    }

    @Override
    public void displayInfo() {
        System.out.println("名前: " + getName());
        System.out.println("労働時間: " + hoursWorked + "時間");
        System.out.println("時給: " + hourlyRate + "円");
        System.out.println("総支給額: " + calculateSalary() + "円");
    }
}

public class EmployeeTest {
    public static void main(String[] args) {
        Employee[] employees = {
            new FullTimeEmployee("山田太郎", 300000, 50000),
            new PartTimeEmployee("鈴木花子", 120, 1500)
        };

        for (Employee employee : employees) {
            employee.displayInfo();
            System.out.println("---");
        }
    }
}

中級問題4: スーパークラスのメソッド呼び出し 解答例

abstract class ElectronicDevice {
    public void turnOn() {
        System.out.println("電子機器の電源を入れます");
    }

    public void turnOff() {
        System.out.println("電子機器の電源を切ります");
    }

    public abstract void displayStartupScreen();
}

class SmartPhone extends ElectronicDevice {
    @Override
    public void displayStartupScreen() {
        System.out.println("スマートフォンの起動画面を表示");
    }

    @Override
    public void turnOn() {
        super.turnOn(); // 親クラスのメソッドを呼び出し
        displayStartupScreen();
    }

    public void specialShutdown() {
        System.out.println("スマートフォンの電源を切ります");
        // 親クラスのturnOffメソッドを呼び出す
        super.turnOff();
    }
}

public class Main {
    public static void main(String[] args) {
        SmartPhone phone = new SmartPhone();
        phone.turnOn();
        phone.displayStartupScreen();
        phone.specialShutdown();
    }
}

中級問題5: ポリモーフィズムの理解 解答例

出力結果:

動物が食事をしています
ワンワン
ボールを取ってきます
動物が食事をしています
ニャー

理由:

  • animal.eat(): 親クラスの具象メソッドが呼び出される
  • animal.makeSound(): 各子クラスでオーバーライドされたメソッドが呼び出される
  • dog.fetch(): 型チェック(instanceof)後にダウンキャストしてDogクラス独自のメソッドを呼び出す
  • Catオブジェクトではinstanceof Dogがfalseになるため、fetch()は呼び出されない
  • climb()メソッドは呼び出されていない

中級問題6: フィールドの隠蔽とメソッドオーバーライド 解答例

出力結果:

Child: 子の値
Child: 変更された子の値
変更された子の値

理由:

  1. obj.printValue(): 子クラスでオーバーライドされたメソッドが呼び出され、子クラスのvalueが表示される
  2. obj.setValue(): 子クラスのsetValue()メソッドが呼び出され、子クラスのvalueフィールドが変更される
  3. 再度obj.printValue(): 変更後の子クラスのvalueが表示される
  4. ((Child) obj).value: ダウンキャストして直接子クラスのフィールドにアクセス

重要なポイント:

  • フィールドはオーバーライドされず「隠蔽」される
  • メソッドはオーバーライドされ、実行時のオブジェクト型に基づいて呼び出される
  • 親クラス型の変数で子クラスオブジェクトを参照する場合、フィールドはコンパイル時の型、メソッドは実行時の型に基づく

上級問題 解答例

上級問題1: テンプレートメソッドパターン 解答例

abstract class Beverage {
    // テンプレートメソッド - 手順を固定
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    // 共通の実装
    private void boilWater() {
        System.out.println("お湯を沸かします");
    }

    private void pourInCup() {
        System.out.println("カップに注ぎます");
    }

    // サブクラスで実装する抽象メソッド
    protected abstract void brew();
    protected abstract void addCondiments();
}

class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println("コーヒーをドリップします");
    }

    @Override
    protected void addCondiments() {
        System.out.println("砂糖とミルクを追加します");
    }
}

class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println("紅茶を浸します");
    }

    @Override
    protected void addCondiments() {
        System.out.println("レモンを追加します");
    }
}

public class BeverageTest {
    public static void main(String[] args) {
        Beverage coffee = new Coffee();
        Beverage tea = new Tea();

        System.out.println("=== コーヒーを準備 ===");
        coffee.prepareRecipe();

        System.out.println("\n=== お茶を準備 ===");
        tea.prepareRecipe();
    }
}

上級問題2: 状態管理システム 解答例

abstract class DocumentState {
    protected Document document;

    public DocumentState(Document document) {
        this.document = document;
    }

    public abstract void publish();
    public abstract void archive();
    public abstract void draft();
    public abstract String getStateName();
}

class DraftState extends DocumentState {
    public DraftState(Document document) {
        super(document);
    }

    @Override
    public void publish() {
        System.out.println("文書を公開状態に変更します");
        document.setState(new PublishedState(document));
    }

    @Override
    public void archive() {
        System.out.println("下書き状態からは直接アーカイブできません");
    }

    @Override
    public void draft() {
        System.out.println("既に下書き状態です");
    }

    @Override
    public String getStateName() {
        return "下書き";
    }
}

class PublishedState extends DocumentState {
    public PublishedState(Document document) {
        super(document);
    }

    @Override
    public void publish() {
        System.out.println("既に公開状態です");
    }

    @Override
    public void archive() {
        System.out.println("文書をアーカイブ状態に変更します");
        document.setState(new ArchivedState(document));
    }

    @Override
    public void draft() {
        System.out.println("公開状態から下書き状態に戻すことはできません");
    }

    @Override
    public String getStateName() {
        return "公開";
    }
}

class ArchivedState extends DocumentState {
    public ArchivedState(Document document) {
        super(document);
    }

    @Override
    public void publish() {
        System.out.println("アーカイブ状態からは直接公開できません");
    }

    @Override
    public void archive() {
        System.out.println("既にアーカイブ状態です");
    }

    @Override
    public void draft() {
        System.out.println("文書を下書き状態に戻します");
        document.setState(new DraftState(document));
    }

    @Override
    public String getStateName() {
        return "アーカイブ";
    }
}

class Document {
    private DocumentState state;
    private String title;

    public Document(String title) {
        this.title = title;
        this.state = new DraftState(this);
    }

    public void setState(DocumentState state) {
        this.state = state;
    }

    public void publish() {
        state.publish();
    }

    public void archive() {
        state.archive();
    }

    public void draft() {
        state.draft();
    }

    public void displayStatus() {
        System.out.println("文書: " + title + " [状態: " + state.getStateName() + "]");
    }
}

public class DocumentStateTest {
    public static void main(String[] args) {
        Document document = new Document("技術ドキュメント");

        document.displayStatus();

        document.publish();  // 下書き → 公開
        document.displayStatus();

        document.archive();  // 公開 → アーカイブ
        document.displayStatus();

        document.draft();    // アーカイブ → 下書き
        document.displayStatus();

        // 無効な遷移の試行
        document.archive();  // 下書き → アーカイブ(不可)
        document.displayStatus();
    }
}

上級問題3: 複雑な継承階層 解答例

abstract class GameCharacter {
    protected String name;
    protected int health;
    protected int maxHealth;

    public GameCharacter(String name, int maxHealth) {
        this.name = name;
        this.maxHealth = maxHealth;
        this.health = maxHealth;
    }

    public abstract void attack(GameCharacter target);
    public abstract void takeDamage(int damage);

    public boolean isAlive() {
        return health > 0;
    }

    public void displayStatus() {
        System.out.println(name + " - HP: " + health + "/" + maxHealth);
    }

    public String getName() {
        return name;
    }
}

abstract class PlayableCharacter extends GameCharacter {
    protected int level;
    protected int experience;

    public PlayableCharacter(String name, int maxHealth, int level) {
        super(name, maxHealth);
        this.level = level;
        this.experience = 0;
    }

    public abstract void useSpecialAbility(GameCharacter target);

    public void gainExperience(int exp) {
        experience += exp;
        System.out.println(name + "は" + exp + "経験値を獲得しました!");
        // レベルアップ処理(簡略化)
        if (experience >= level * 100) {
            level++;
            maxHealth += 20;
            health = maxHealth;
            System.out.println(name + "がレベル" + level + "に上がりました!");
        }
    }

    @Override
    public void displayStatus() {
        super.displayStatus();
        System.out.println("レベル: " + level + " EXP: " + experience);
    }
}

abstract class NPC extends GameCharacter {
    protected String dialogue;

    public NPC(String name, int maxHealth, String dialogue) {
        super(name, maxHealth);
        this.dialogue = dialogue;
    }

    public abstract void interact();

    public void speak() {
        System.out.println(name + ": 「" + dialogue + "」");
    }
}

class Warrior extends PlayableCharacter {
    private int strength;

    public Warrior(String name, int level) {
        super(name, 100 + (level * 20), level);
        this.strength = 10 + (level * 2);
    }

    @Override
    public void attack(GameCharacter target) {
        int damage = strength + (int)(Math.random() * 10);
        System.out.println(name + "の剣の攻撃!");
        target.takeDamage(damage);
    }

    @Override
    public void useSpecialAbility(GameCharacter target) {
        if (level >= 3) {
            int damage = strength * 2;
            System.out.println(name + "の必殺技・乱れ撃ち!");
            target.takeDamage(damage);
        } else {
            System.out.println("レベルが足りません!");
            attack(target);
        }
    }

    @Override
    public void takeDamage(int damage) {
        int reducedDamage = damage - (level * 2);
        if (reducedDamage < 1) reducedDamage = 1;
        health -= reducedDamage;
        System.out.println(name + "は" + reducedDamage + "ダメージを受けた!");
    }
}

class Mage extends PlayableCharacter {
    private int intelligence;

    public Mage(String name, int level) {
        super(name, 60 + (level * 15), level);
        this.intelligence = 12 + (level * 3);
    }

    @Override
    public void attack(GameCharacter target) {
        int damage = intelligence + (int)(Math.random() * 8);
        System.out.println(name + "の魔法攻撃!");
        target.takeDamage(damage);
    }

    @Override
    public void useSpecialAbility(GameCharacter target) {
        if (level >= 5) {
            int damage = intelligence * 3;
            System.out.println(name + "の究極魔法・メテオストライク!");
            target.takeDamage(damage);
        } else {
            System.out.println("レベルが足りません!");
            attack(target);
        }
    }

    @Override
    public void takeDamage(int damage) {
        health -= damage;
        System.out.println(name + "は" + damage + "ダメージを受けた!");
    }
}

class Merchant extends NPC {
    private int gold;

    public Merchant(String name) {
        super(name, 50, "いらっしゃい、何かお探しですか?");
        this.gold = 1000;
    }

    @Override
    public void attack(GameCharacter target) {
        System.out.println(name + "は商人なので戦えません!");
    }

    @Override
    public void takeDamage(int damage) {
        health -= damage;
        System.out.println(name + "は" + damage + "ダメージを受けた!");
        if (isAlive()) {
            System.out.println("た、助けてください!");
        }
    }

    @Override
    public void interact() {
        speak();
        System.out.println(name + "は様々な商品を販売しています");
    }
}

class Enemy extends NPC {
    private int attackPower;

    public Enemy(String name, int maxHealth, int attackPower) {
        super(name, maxHealth, "がおおお!倒してやる!");
        this.attackPower = attackPower;
    }

    @Override
    public void attack(GameCharacter target) {
        int damage = attackPower + (int)(Math.random() * 5);
        System.out.println(name + "の攻撃!");
        target.takeDamage(damage);
    }

    @Override
    public void takeDamage(int damage) {
        health -= damage;
        System.out.println(name + "は" + damage + "ダメージを受けた!");
    }

    @Override
    public void interact() {
        speak();
        System.out.println(name + "は戦闘態勢をとった!");
    }
}

public class GameBattleTest {
    public static void main(String[] args) {
        // キャラクター作成
        PlayableCharacter warrior = new Warrior("勇者アーサー", 5);
        PlayableCharacter mage = new Mage("魔導師メリン", 4);
        NPC merchant = new Merchant("道具屋のジョン");
        NPC enemy = new Enemy("ゴブリン", 80, 15);

        // バトルシミュレーション
        System.out.println("=== バトル開始 ===");

        GameCharacter[] characters = {warrior, mage, merchant, enemy};

        // ステータス表示
        for (GameCharacter character : characters) {
            character.displayStatus();
        }
        System.out.println();

        // 戦闘
        warrior.attack(enemy);
        enemy.displayStatus();
        System.out.println();

        enemy.attack(warrior);
        warrior.displayStatus();
        System.out.println();

        mage.useSpecialAbility(enemy);
        enemy.displayStatus();
        System.out.println();

        // 経験値獲得
        if (!enemy.isAlive()) {
            warrior.gainExperience(50);
            mage.gainExperience(50);
        }

        System.out.println();

        // インタラクション
        merchant.interact();
        System.out.println();

        // 最終ステータス
        warrior.displayStatus();
        mage.displayStatus();
    }
}