こんにちは、かつコーチです。
前回はオーバーライドとポリモーフィズムを解説しました。
今回は、「共通の設計だけ決めて、具体的な実装は子クラスに任せたい」というときに使う抽象クラス(abstract)について解説します。
抽象クラスとは?
インスタンス化できない特殊なクラス
抽象クラスとは、abstractキーワードを付けて宣言する、単体ではnewできない特殊なクラスのことです。
「このクラスは設計の土台として使うためのもので、そのままでは完成していない」ということを示します。
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 + "が眠りました");
}
}
Animal animal = new Animal("謎の生物"); // コンパイルエラー:抽象クラスはインスタンス化できない
error: Animal is abstract; cannot be instantiated
抽象メソッドとは
abstract void makeSound();のように、処理内容({}の中身)を書かず宣言だけを行うメソッドを「抽象メソッド」と呼びます。
抽象メソッドを持つクラスは、必ずabstractクラスとして宣言する必要があります。
抽象クラスを継承する子クラスは、すべての抽象メソッドを実装(オーバーライド)しなければなりません。
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() { // 実装しないとコンパイルエラーになる
System.out.println(name + "がワンと吠えました");
}
}
もしmakeSound()を実装しないと、以下のようなコンパイルエラーになります。
error: Dog is not abstract and does not override abstract method makeSound() in Animal
なぜ抽象クラスが必要なのか
「実装を強制したい」場面で使う
前回の記事のAnimalクラスは通常のクラス(abstractなし)でした。
もしAnimalにmakeSound()の中身を空のまま定義しておき、子クラスが実装を忘れても気づけません。
❌Before(通常クラスで実装忘れに気づけない)
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
// 中身を空にしておくと、子クラスが実装を忘れてもコンパイルは通ってしまう
}
}
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
// makeSound()の実装を忘れている
}
Animal bird = new Bird("ピヨ");
bird.makeSound(); // 何も起こらない(バグに気づきにくい)
✅After(abstractで実装を強制する)
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void makeSound(); // 実装を子クラスに強制する
}
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
// makeSound()を実装しないとコンパイルエラーになるため、実装忘れに気づける
}
abstractにしておくことで、「実装を忘れる」というミスをコンパイル時に検出できるようになります。
これは実務でも非常によく使われる設計テクニックです。
よくあるつまずきポイント・エラー対処
抽象クラスにコンストラクタを書いてしまい混乱する
「抽象クラスはインスタンス化できないのに、コンストラクタを書く意味があるのか」と疑問を持つ人が多いです。
実は抽象クラスにもコンストラクタを定義でき、子クラスのsuper(...)から呼び出されます。
public abstract class Animal {
protected String name;
public Animal(String name) { // 抽象クラスでもコンストラクタは書ける
this.name = name;
}
public abstract void makeSound();
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // 抽象クラスのコンストラクタを呼び出せる
}
@Override
public void makeSound() {
System.out.println(name + "がワンと吠えました");
}
}
抽象クラス自体はnewできませんが、子クラスをインスタンス化する過程で親(抽象クラス)のコンストラクタは必ず実行されます。
私も学習初期に「抽象クラスはコンストラクタを持てない」と誤解していましたが、実際には共通の初期化処理をまとめる場所として非常に有効に使えます。
abstractメソッドに中身を書いてしまう
❌Before
public abstract class Animal {
public abstract void makeSound() {
System.out.println("鳴きました"); // コンパイルエラー
}
}
error: abstract methods cannot have a body
✅After
抽象メソッドには{}を書かず、セミコロンで終わらせます。
共通処理を持たせたい場合は、抽象メソッドとは別に通常のメソッドとして定義します。
public abstract class Animal {
public abstract void makeSound(); // 中身なし
public void sleep() { // 通常メソッドなら中身を書ける
System.out.println("眠りました");
}
}
応用・一歩先の使い方
テンプレートメソッドパターンとの組み合わせ
抽象クラスは、処理の骨格を親クラスで決めておき、詳細な一部分だけを子クラスに任せる「テンプレートメソッドパターン」でよく使われます。
public abstract class ReportGenerator {
// 処理の流れ(テンプレート)は親クラスで固定する
public final void generate() {
printHeader();
printBody(); // ここだけ子クラスごとに変わる
printFooter();
}
private void printHeader() {
System.out.println("=== レポート開始 ===");
}
protected abstract void printBody(); // 子クラスに実装を強制
private void printFooter() {
System.out.println("=== レポート終了 ===");
}
}
public class SalesReport extends ReportGenerator {
@Override
protected void printBody() {
System.out.println("売上: 100万円");
}
}
new SalesReport().generate();
=== レポート開始 ===
売上: 100万円
=== レポート終了 ===
generate()メソッド自体はfinalにして子クラスに上書きさせず、「変えてよい部分(printBody)」だけを抽象メソッドとして開放しています。
このように「変えたくない流れ」と「変えてよい部分」を明確に分けられるのが、抽象クラスの強力な使い方です。
まとめ
この記事のポイント
- 抽象クラスは
abstractを付けたクラスで、単体ではnewできない - 抽象メソッドは処理内容を書かず宣言のみを行い、子クラスに実装を強制する
- 抽象クラスにもコンストラクタを定義でき、子クラスの
super(...)から呼び出される - テンプレートメソッドパターンのように、共通の流れを固定しつつ一部だけ子クラスに委ねる設計に向いている
次に読むべき記事
抽象クラスの使い方を理解したら、次はさらに柔軟な設計を可能にする「interfaceの基本」に進みましょう。
タグ: Java, 中級者向け, オブジェクト指向