Java | オブジェクト指向:抽象クラスとは

Java Java
スポンサーリンク

抽象クラスとは

抽象クラスは「直接はインスタンス化できない、共通の骨格と契約をまとめるためのクラス」です。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 にして、差し替えは小さなメソッドに限定すると「壊れやすい基底クラス問題」を抑制できます。


仕上げのアドバイス(重要部分のまとめ)

抽象クラスは「共通の契約・前処理・状態」を親でまとめ、子に“差分だけ”を実装させるための土台です。抽象メソッドで必須の差し替え点を定め、テンプレートメソッドで流れを固定すると、安全で再利用性の高い設計になります。インターフェースとは目的が異なるため、状態管理や不変条件が必要なら抽象クラス、役割の組み合わせならインターフェースを選ぶのが筋です。

タイトルとURLをコピーしました