抽象クラスの狙いどころ
抽象クラスは「共通の骨格と不変条件を親で固定し、差し替えたい部分だけ子に実装させる」ための道具です。インターフェースが“契約(メソッドの型)”だけなのに対し、抽象クラスは“状態(フィールド)と共通実装”まで一緒に持てます。これにより、前処理・検証・監査・初期化の順序を親で統一しつつ、具体的なやり方は子に任せる安全な拡張が可能になります。
テンプレートメソッドで「流れ」を固定する(重要)
テンプレートメソッドは、親が処理の流れ(骨格)を定義し、差し替えたいステップだけ抽象メソッドにするパターンです。流れを固定しておくと、監査や検証が抜ける事故を親で防げます。
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);
}
final class CsvImporter extends Importer {
@Override protected String load(String path) { return "a,b,c"; }
@Override protected void save(String data) { System.out.println("saved: " + data); }
}
Javaこの形なら、仕様変更(例: normalize を強化)は親の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共通検証は親に、具体実装は子に——この分離で「抜け漏れ」を機械的に防げます。
Skeletal implementation(骨格実装)で高速に増やす
“骨格実装”とは、抽象クラスに最低限の共通実装と便利メソッドを揃え、子は差分だけ実装すれば完成する形のことです。標準ライブラリの AbstractList などが典型です。自作でも「必須の抽象メソッド+デフォルトの具象メソッド」を用意すると、新しい派生型を短時間で安全に増やせます。
abstract class Formatter {
public final String format(String raw) {
String s = preprocess(raw); // 既定処理
return postprocess(s); // 抽象:仕上げだけ差し替え
}
protected String preprocess(String s) { return s == null ? "" : s.trim().replaceAll("\\s+", " "); }
protected abstract String postprocess(String s);
}
final class UpperFormatter extends Formatter {
@Override protected String postprocess(String s) { return s.toUpperCase(); }
}
Java骨格があるから、子は「中核の1メソッド」を書くだけで完成します。拡張速度と一貫性が向上します。
インターフェースとの使い分け(重要)
抽象クラスは「状態を持ち、初期化順序や不変条件を親で管理したい」ときに向いています。インターフェースは「複数の役割を自由に組み合わせたい」場面に強いです。両者の併用が実務では最適解になりがちです。
interface Normalizer { String apply(String s); }
abstract class SecureService implements Normalizer {
@Override public final String apply(String s) {
String t = s == null ? "" : s.trim();
audit("normalize");
return doApply(t); // 抽象:差し替え点
}
protected abstract String doApply(String s);
private void audit(String tag) { System.out.println("[AUDIT] " + tag); }
}
final class LowerService extends SecureService {
@Override protected String doApply(String s) { return s.toLowerCase(); }
}
Java役割(Normalizer)はインターフェースで提供しつつ、流れ・監査・状態は抽象クラスで固定する——組み合わせると強力です。
プラグイン構造・ポリモーフィズムの土台にする
抽象クラスを「共通の起動手順」「エラーハンドリング」「リソース管理」の土台にし、子が“実処理”だけを提供すると、差し替え可能なプラグイン構造が簡潔に作れます。呼び出し側は親型で扱うだけで、実体に応じて動的に切り替わります。
abstract class Task {
public final void run() {
try {
setup();
execute(); // 抽象:プラグインの本体
} finally {
teardown();
}
}
protected void setup() {}
protected abstract void execute();
protected void teardown() {}
}
final class EmailTask extends Task {
@Override protected void execute() { System.out.println("send email"); }
}
final class SmsTask extends Task {
@Override protected void execute() { System.out.println("send sms"); }
}
Java安全な枠組み(setup/teardown)を親で保証し、拡張は execute だけに集中できます。
落とし穴と守るべき型(深掘り)
コンストラクタでオーバーライド可能メソッド(抽象や非 final)を呼ぶのは危険です。多段継承で子の未初期化フィールドに触れてしまいます。初期化は「親の完成 → 子の完成」を崩さず、仮想呼び出しを避けてください。また、protected フィールドの露出はカプセル化を壊します。フィールドは private、拡張ポイントは検証付きの protected メソッドに限定し、流れを担うメソッドは final にするのが安全です。継承が深くなるなら、階層を浅く保ち、命名と @Override を徹底して読解コストを抑えます。
仕上げのアドバイス(重要部分のまとめ)
抽象クラスは「流れ・不変条件・共通状態」を親で固め、差し替えたい小さな点だけ子に実装させる武器です。テンプレートメソッドで骨格を固定し、骨格実装で拡張を高速化する。インターフェースと組み合わせて役割を柔軟にしつつ、安全性は親で担保する。コンストラクタの仮想呼び出しを避け、拡張ポイントは最小に——この型を守れば、拡張しやすく壊れにくい設計が自然に手に入ります。
