PR

Java:抽象クラス(abstract class)を3分でわかりやすく

Java

「抽象クラス」という概念は、オブジェクト指向プログラミングの基礎を学ぶ上で非常に重要なテーマのひとつです。抽象クラスは、クラスの設計において、共通部分をまとめて管理しコードの再利用性や保守性を向上させるための仕組みとして用いられます。

このページでは、抽象クラスとは何か、どのように定義・利用するのか、なぜそれが役立つのかを、実例を交えながら順を追って解説していきます。

スポンサーリンク

抽象クラスとは何か

現実世界には多くの物事が存在し、それらは共通の特徴や性質を持っていることがあります。例えば動物であれば犬や猫・鳥などがあり、それぞれの動物は「鳴く」「歩く」といった共通の行動を持っています。(各動物が実際にどのように鳴くか、歩くかは異なります。)こうした共通部分をひとまとめにし、具体的な違いだけを個別に扱う設計方法が「抽象化」と呼ばれます。

抽象クラスは、この抽象化の考え方をプログラム上に実現するためのものです。つまり、抽象クラスは、具体的な実装を持たない部分(=抽象的な部分)を定義し、そこから派生する具体的なクラスが詳細な実装を行うように設計する仕組みです。

トヨタも抽象クラス的な発想をしている?

トヨタの自動車も抽象クラス的な発想で作られていると言えるかもしれません。トヨタでは、いくつかの車種をグルーピング化し、その車種に共通する車のフレームやエンジンなどが一体化した「基本セット」のようなものを開発しました。

具体的な車を開発する際はこの「基本セット」の中からいずれかを選択し、その「基本セット」の上に内装や細かなチューニングを行う!という手法で開発を進めています。

この「基本セット」に当たるのが今回の抽象クラスといえます。

抽象クラスの役割

抽象クラスは、以下のような役割を果たします。

  1. 共通の設計図の提供
    複数のクラスに共通する属性や機能をひとまとめにして記述することができます。これにより、各クラスで同じようなコードを重複して書かなくても済むようになります。
  2. インターフェースの契約
    抽象クラス内に抽象メソッドを定義することで、そのクラスを継承するすべての具象クラス(実際にインスタンス化可能なクラス)に対して、必ず実装しなければならないメソッドを強制することができます。
  3. コードの再利用性の向上
    抽象クラスに共通のフィールドや具体的なメソッドを実装しておけば、サブクラスはそのまま利用でき、必要な部分だけを上書きすることで機能を追加・変更することができます。

なんでわざわざそんなことをするのか・・・?は実装していく中で徐々に理解が深まるかと思います。そのためにもまずは抽象クラスの使い方を学習しておきましょう。

抽象クラスの定義方

Javaでは、クラスの定義の前に「abstract」というキーワードを付けることで、そのクラスが抽象クラスであることを示します。

以下が基本的な抽象クラスの定義例。

public abstract class Animal {
    protected String name;  // 動物の名前など、共通の属性

    // コンストラクタ:抽象クラスでもコンストラクタは定義でき、サブクラスの初期化に使う
    public Animal(String name) {
        this.name = name;
    }

    // 抽象メソッド:具体的な実装はサブクラスに任せる
    public abstract void makeSound();

    // 具体的なメソッド:全ての動物に共通の振る舞いを実装できる
    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

上記の例では、Animal クラスは抽象クラスとして定義されています。makeSound() メソッドは抽象メソッドであり、実際にどのような音を出すかは、Animal を継承したサブクラスで決定することになります。

ちなみに、抽象クラスは、直接インスタンス化(オブジェクトを作成)することはできません

なぜなら、抽象クラスは不完全な設計図であり、具体的な動作が未定義な部分があるからです。

抽象メソッド

抽象メソッドとは、メソッドのシグネチャ(名前、引数、戻り値の型)だけを定義し、メソッド本体が存在しないメソッドです。抽象クラス内に抽象メソッドがある場合、必ずその抽象クラスを継承する具象クラスは、そのメソッドをオーバーライドして具体的な処理を記述しなければなりません。

これにより、プログラム全体の設計上、どの具象クラスも同じインターフェース(メソッド名や引数の形式)で動作することが保証され、コードの整合性や多態性(ポリモーフィズム)を実現することができます。

抽象クラスを使った具体的な実装例

ここからは、先ほどの Animal クラスを継承して、実際に動物の種類ごとの実装を行う例を示します。例えば、犬(Dog)と猫(Cat)のクラスを作成する場合を考えてみましょう。

サンプルコード Dog クラスの実装

public class Dog extends Animal {
    public Dog(String name) {
        super(name);  // Animalクラスのコンストラクタを呼び出す
    }

    // 抽象メソッド makeSound() の実装
    @Override
    public void makeSound() {
        System.out.println(name + " says: Woof!");
    }
}

この例では、Dog クラスは Animal クラスを継承しており、makeSound() メソッドを具体的に実装しています。これにより、Dog オブジェクトは「Woof!」という音を出す動作を持つことになります。

サンプルコード Cat クラスの実装

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    // 抽象メソッド makeSound() の実装
    @Override
    public void makeSound() {
        System.out.println(name + " says: Meow!");
    }
}

こちらは Cat クラスの例です。犬の例と同様に、Animal クラスを継承し、makeSound() メソッドに猫特有の動作(Meow!)を実装しています。

抽象クラスの利用例:ポリモーフィズムの実現

抽象クラスは、ポリモーフィズム(多態性)を利用する際にも非常に役立ちます。以下のようなプログラムを考えてみましょう。

public class Main {
    public static void main(String[] args) {
        // Animal型の配列にDogやCatのインスタンスを代入する
        Animal[] animals = {
            new Dog("Buddy"),
            new Cat("Misty")
        };

        // それぞれの動物の音を出させる
        for (Animal animal : animals) {
            animal.makeSound();
            animal.sleep();
        }
    }
}

このコードでは、Animal 型の変数に DogCat のオブジェクトを格納しています。ループで各オブジェクトに対して makeSound() を呼び出すと、実際には Dog クラスまたは Cat クラスで実装された内容が実行されます。これがポリモーフィズムの基本的な考え方であり、抽象クラスを用いることで共通の操作を統一的に扱えるメリットとなります。

抽象クラスのメリットとデメリット

抽象クラスのメリット

  1. 共通コードの再利用
    抽象クラスに共通のフィールドやメソッドを実装しておくことで、サブクラスで同じコードを重複して書く必要がなくなります。これにより、メンテナンスが容易になり、バグが発生するリスクも低減されます。
  2. 設計の一貫性の向上
    抽象メソッドを利用することで、継承するすべてのクラスに共通のインターフェースを提供できます。これにより、開発者はどのクラスも同じ方法で利用できると理解でき、コードの一貫性が保たれます。
  3. 柔軟な拡張性
    抽象クラスを基に新たな具象クラスを作成する場合、既存の共通機能をそのまま利用できるため、システムの拡張が容易になります。新しいクラスを追加する際は、必要な部分だけをオーバーライドすればよいため、設計変更の影響範囲を限定できます。
  4. 保守性の向上
    共通の機能を一箇所で管理できるため、後から仕様変更やバグ修正があった場合も、抽象クラス側を修正するだけで済むケースが多くなります。

抽象クラスのデメリット

  1. 単一継承の制約
    Javaではクラスの多重継承は許されていません。つまり、あるクラスがすでに別の抽象クラスや具象クラスを継承している場合、さらに別の抽象クラスを継承することはできません。そのため、設計によっては柔軟性が制限される場合があります。
  2. 抽象クラスの変更がサブクラスに影響を及ぼす
    抽象クラスの仕様変更は、それを継承している全てのサブクラスに影響を与える可能性があります。設計段階で抽象クラスの責務や役割を十分に考え、変更が最小限に留まるように工夫する必要があります。
  3. 抽象クラスとインターフェースの使い分け
    抽象クラスは具体的な実装も持てるため、場合によってはインターフェースで十分なケースも存在します。インターフェースは多重実装が可能であり、柔軟性が高いため、抽象クラスとの使い分けを明確にする必要があります。

抽象クラスとインターフェースの違い

Javaでは、抽象クラスとインターフェースという2つの概念があり、どちらもクラス間の共通の設計を提供するために用いられますが、いくつかの点で大きな違いがあります。

状態(フィールド)の保持

  • 抽象クラス
    抽象クラスは、フィールド(変数)を持つことができ、コンストラクタも定義できます。これにより、サブクラス間で共通する状態やデータを保持することができます。
  • インターフェース
    インターフェースは基本的にメソッドの宣言のみを行います。Java 8以降は、デフォルトメソッドやstaticメソッドが追加されましたが、状態を持つことはできません。すべてのフィールドは暗黙的に public static final として定義されるため、実質的に定数しか持つことができません。

継承の仕組み

  • 抽象クラス
    Javaではクラスの多重継承はできないため、1つのクラスは1つの抽象クラス(または具象クラス)しか継承できません。そのため、抽象クラスは単一継承の枠組みの中で利用されます。
  • インターフェース
    複数のインターフェースを1つのクラスで実装することが可能です。これにより、あるクラスが複数の異なる契約を同時に満たすことができます。

利用する目的

  • 抽象クラス
    共通の処理や状態、振る舞いをまとめるために利用され、サブクラスに対して部分的な実装を提供することができます。コードの再利用性や一貫性を重視する場合に有効です。
  • インターフェース
    クラス間の共通の操作や振る舞いの契約を定義するために用いられます。実装の詳細には関与せず、クラスがどのような機能を提供するかを保証するための仕組みとして使われます。

JVM(Java仮想マシン)における抽象クラスの動作原理

抽象クラスは、Javaのコンパイル時から実行時にかけて、いくつかの仕組みで処理されています。ここでは、その大まかな流れをわかりやすく説明します。

コンパイル時のチェック

Javaコンパイラは、抽象クラスに定義された抽象メソッドのシグネチャを確認し、サブクラスがこれらのメソッドを必ずオーバーライドしているかをチェックします。もしサブクラスが抽象メソッドを実装していなければ、そのサブクラスも抽象クラスとして扱われ、インスタンス化できなくなります。これにより、プログラム全体の整合性が保証されます。

クラスローディングとリンク

プログラムの実行時、JVMは必要なクラスをメモリに読み込みます。抽象クラス自体は直接インスタンス化されないため、実体としてのオブジェクトは作成されませんが、共通のフィールドやメソッドの実装情報は、継承関係を通じてサブクラスに引き継がれます。

動的ディスパッチ(遅延バインディング)

実行時に、変数の実際の型に応じたメソッドが呼び出される仕組みを「動的ディスパッチ」と言います。たとえば、Animal 型の変数に Dog オブジェクトを代入している場合、makeSound() を呼び出すと、実際には Dog クラスで実装されたメソッドが実行されます。これにより、プログラムは実行時に正しいメソッドを選択することができます。

最適化の仕組み

JVMには、JIT(Just-In-Time)コンパイラという仕組みがあり、頻繁に呼ばれるメソッドは最適化され、インライン展開されることがあります。抽象クラスで定義された具体的なメソッドも、実際の実装が確定すればこの最適化対象となり、プログラムの実行速度に影響を与えないように工夫されています。

まとめ Javaの抽象クラスとは?

  • 抽象クラスの定義
    抽象クラスは「abstract」キーワードを使って宣言され、直接インスタンス化できません。共通のフィールドやメソッド、そして抽象メソッドを定義するための設計図として機能します。
  • 抽象メソッドの役割
    抽象メソッドは、メソッドの名前や引数などの基本情報だけを定義し、具体的な処理はサブクラスに任せる仕組みです。これにより、どのサブクラスも同じメソッドを実装しなければならず、統一性が保たれます。
  • 実際の実装例
    例えば、Animal クラスという抽象クラスを定義し、そこから DogCat といった具象クラスを作成することで、各動物の固有の鳴き声や振る舞いを実装できます。こうした実装例は、ポリモーフィズムの仕組みを利用して、同じ抽象型の変数で複数の具象クラスを扱えるようにします。
  • JVMでの動作原理
    コンパイル時のチェック、クラスローディング、動的ディスパッチ、そしてJITコンパイラによる最適化といった仕組みにより、抽象クラスを利用したプログラムは実行時に正しく動作します。
  • 抽象クラスとインターフェースの違い
    抽象クラスは状態(フィールド)を持ち、部分的な実装も提供できる一方で、インターフェースは主にメソッドの契約だけを定義し、複数実装が可能な点が特徴です。設計の意図に応じてどちらを使うかを選択する必要があります。
  • 利用時の注意点
    設計段階での役割の明確化、継承関係の深さへの配慮、インターフェースとの使い分け、そして仕様変更時の影響範囲の確認が必要です。これらの注意点を守ることで、抽象クラスのメリットを最大限に活用できます。
タイトルとURLをコピーしました