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

このページでは、抽象クラスとは何か、どのように定義・利用するのか、なぜそれが役立つのかを、実例を交えながら順を追って解説していきます。
抽象クラスとは何か
現実世界には多くの物事が存在し、それらは共通の特徴や性質を持っていることがあります。例えば動物であれば犬や猫・鳥などがあり、それぞれの動物は「鳴く」「歩く」といった共通の行動を持っています。(各動物が実際にどのように鳴くか、歩くかは異なります。)こうした共通部分をひとまとめにし、具体的な違いだけを個別に扱う設計方法が「抽象化」と呼ばれます。
抽象クラスは、この抽象化の考え方をプログラム上に実現するためのものです。つまり、抽象クラスは、具体的な実装を持たない部分(=抽象的な部分)を定義し、そこから派生する具体的なクラスが詳細な実装を行うように設計する仕組みです。
トヨタの自動車も抽象クラス的な発想で作られていると言えるかもしれません。トヨタでは、いくつかの車種をグルーピング化し、その車種に共通する車のフレームやエンジンなどが一体化した「基本セット」のようなものを開発しました。
具体的な車を開発する際はこの「基本セット」の中からいずれかを選択し、その「基本セット」の上に内装や細かなチューニングを行う!という手法で開発を進めています。
この「基本セット」に当たるのが今回の抽象クラスといえます。
抽象クラスの役割
抽象クラスは、以下のような役割を果たします。
- 共通の設計図の提供
複数のクラスに共通する属性や機能をひとまとめにして記述することができます。これにより、各クラスで同じようなコードを重複して書かなくても済むようになります。 - インターフェースの契約
抽象クラス内に抽象メソッドを定義することで、そのクラスを継承するすべての具象クラス(実際にインスタンス化可能なクラス)に対して、必ず実装しなければならないメソッドを強制することができます。 - コードの再利用性の向上
抽象クラスに共通のフィールドや具体的なメソッドを実装しておけば、サブクラスはそのまま利用でき、必要な部分だけを上書きすることで機能を追加・変更することができます。

なんでわざわざそんなことをするのか・・・?は実装していく中で徐々に理解が深まるかと思います。そのためにもまずは抽象クラスの使い方を学習しておきましょう。
抽象クラスの定義方
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
型の変数に Dog
や Cat
のオブジェクトを格納しています。ループで各オブジェクトに対して makeSound()
を呼び出すと、実際には Dog
クラスまたは Cat
クラスで実装された内容が実行されます。これがポリモーフィズムの基本的な考え方であり、抽象クラスを用いることで共通の操作を統一的に扱えるメリットとなります。
抽象クラスのメリットとデメリット
抽象クラスのメリット
抽象クラスのデメリット
抽象クラスとインターフェースの違い
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)コンパイラという仕組みがあり、頻繁に呼ばれるメソッドは最適化され、インライン展開されることがあります。抽象クラスで定義された具体的なメソッドも、実際の実装が確定すればこの最適化対象となり、プログラムの実行速度に影響を与えないように工夫されています。