抽象クラスとは
抽象クラスは「直接はインスタンス化できない、共通の骨格と契約をまとめるためのクラス」です。abstract を付けて宣言し、抽象メソッド(中身のないメソッド)を含められます。継承した子クラスは、その抽象メソッドを必ず実装し、親が決めた型の“約束”に従います。共通の前処理・検証・状態(フィールド)を親で持ちつつ、差し替えたい部分だけを子に任せる設計に向いています。
できること・できないこと(重要)
抽象クラスは new できませんが、コンストラクタは持てます。目的は「子の初期化前に、親の不変条件を確立する」ことです。フィールドや具象メソッド(中身のあるメソッド)を持てるため、共通処理を親で共有しやすくなります。一方、Java は単一継承なので、抽象・具象に関わらず extends は1つだけです。複数の契約を持たせたい場合は、抽象クラスをひとつ継承しつつ、インターフェースを複数実装する組み合わせが一般的です。
抽象メソッドとテンプレートメソッド(重要な深掘り)
抽象メソッドは、親が「この名前・この引数で必ず提供してね」と定める契約です。子が実装しないとコンパイルエラーになります。テンプレートメソッドは、親が処理の流れを定義し、差し替えたいステップだけ抽象や protected で開くパターンです。枠組みを親で固定し、子は最小限の差分だけに集中できるため、再利用性と安全性が高まります。
abstract class Importer {
public final void run(String path) {
String raw = load(path); // 子が実装
String normalized = normalize(raw); // 親の共通処理
save(normalized); // 子が実装
}
protected abstract String load(String path); // 抽象メソッド
protected String normalize(String s) { // 具象メソッド(共通化)
return s == null ? "" : s.trim();
}
protected abstract void save(String data); // 抽象メソッド
}
Java例題 1: 共通の検証と差分の実装
親が共通の検証と状態を持ち、子が“やり方”だけを決める典型例です。親コンストラクタで不変条件を固定してから、子の初期化へ進みます。
abstract class PaymentGateway {
private final String endpoint;
protected PaymentGateway(String endpoint) {
if (endpoint == null || endpoint.isBlank()) throw new IllegalArgumentException("endpoint");
this.endpoint = endpoint.trim();
}
protected final String endpoint() { return endpoint; }
public final boolean charge(int amount) {
if (amount <= 0) return false; // 共通検証
return doCharge(amount); // 差し替えポイント
}
protected abstract boolean doCharge(int amount);
}
final class StripeGateway extends PaymentGateway {
StripeGateway(String endpoint) { super(endpoint); }
@Override protected boolean doCharge(int amount) {
System.out.println("stripe -> " + endpoint() + " amt=" + amount);
return true;
}
}
Javaこの形なら、検証抜けや前処理漏れを親で防ぎ、子の実装を増やしても品質が一定に保てます。
例題 2: 部分実装と共通状態の共有
抽象クラスはフィールドや具象メソッドを持てるので、共通の状態・ユーティリティを親で提供できます。子はそれを活用して振る舞いを仕上げます。
abstract class Describable {
private final String id;
protected Describable(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
this.id = id.trim();
}
protected final String id() { return id; }
public String describe() { // 既定の実装(必要なら上書き可)
return "id=" + id();
}
}
final class User extends Describable {
private final String name;
User(String id, String name) {
super(id);
this.name = (name == null) ? "" : name.trim();
}
@Override public String describe() { // 差し替え(上書き)
return super.describe() + ", name=" + name;
}
}
Java親の状態と共通フォーマットを再利用しつつ、子で必要な情報だけを追加できます。
インターフェースとの違いと使い分け(重要)
抽象クラスは「状態(フィールド)を持てる」「共通実装をまとめられる」点が強みです。インターフェースは「多重実装可能で軽い契約」を定義する道具で、Java 8 以降は default メソッドで簡単な共通実装も持てますが、状態は持ちません。次のように使い分けると設計が安定します。
- 状態や不変条件、初期化の順序を親で管理したいなら抽象クラス。
- 振る舞いの“名前と型”だけを共有し、実装の自由度を最大化したいならインターフェース。
- 単一の系統を作り、処理の骨格を固定して差し替えポイントを限定したいなら抽象+テンプレートメソッド。
- 複数の役割を組み合わせたいなら、抽象クラスを1つ継承しつつ、インターフェースを複数実装。
実務指針と落とし穴(深掘り)
抽象クラスのコンストラクタで、子がオーバーライドする可能性のあるメソッドを呼ぶと、子のフィールドが未初期化のまま参照される危険があります。コンストラクタ内の“仮想呼び出し”は避け、初期化は「親の完成 → 子の完成」の順序を崩さないようにします。また、protected フィールドの露出はカプセル化を壊しやすいので、private フィールド+検証付きの protected メソッドで拡張ポイントを提供するのが安全です。骨格は親で final にして、差し替えは小さなメソッドに限定すると「壊れやすい基底クラス問題」を抑制できます。
仕上げのアドバイス(重要部分のまとめ)
抽象クラスは「共通の契約・前処理・状態」を親でまとめ、子に“差分だけ”を実装させるための土台です。抽象メソッドで必須の差し替え点を定め、テンプレートメソッドで流れを固定すると、安全で再利用性の高い設計になります。インターフェースとは目的が異なるため、状態管理や不変条件が必要なら抽象クラス、役割の組み合わせならインターフェースを選ぶのが筋です。
