抽象メソッドとは
抽象メソッドは「中身(実装)がないメソッド」で、宣言だけを持ちます。abstract を付けて宣言し、具体的な処理は子クラスが必ず実装します。抽象クラスに定義して、共通の“名前・引数・戻り値”という契約を先に決め、やり方の違いは子クラスへ委ねるための仕組みです。これにより、呼び出し側は共通の型で扱いながら、実体に応じて振る舞いが切り替わる多態性が成立します。
使う理由と効果
抽象メソッドを使う最大の理由は「共通の契約を定めて、差し替え可能な拡張点を作る」ことです。親クラスは処理の骨格や前処理・検証を持ち、差し替えたいステップだけ抽象メソッドにします。子クラスが実装を提供することで、呼び出し側に分岐や詳細知識を持たせず、拡張や変更の影響を最小化できます。テストでも、抽象メソッドを実装したテスト用の子クラスに差し替えるだけで、振る舞いをコントロールできます。
基本ルールと宣言の仕方
宣言と実装の必須関係
抽象メソッドは「本体なし」で宣言します。親クラスが abstract である必要があり、子クラスはそのメソッドを実装しないとコンパイルエラーになります。実装したくない子クラスは、子自身を abstract にすればさらに下位での実装に委ねられます。
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使えない修飾子と注意点
抽象メソッドは final・static・private にはできません。final は「上書き禁止」と矛盾し、static はクラスメソッドのため動的切替の対象外、private は子クラスから見えないため契約の強制ができないためです。アクセス修飾子は public/protected/package-private のいずれかを選び、契約の公開範囲を明確にします。
テンプレートメソッドとの連携(重要)
抽象メソッドは、テンプレートメソッドと組み合わせると真価を発揮します。親が処理の流れを final で固定し、差し替えたいステップだけ抽象化することで、安全に拡張できます。前処理・検証・監査など「必ず通してほしい」共通処理は親が持ち、個別の手順は抽象メソッドとして子に任せる形が理想です。
abstract class SecureService {
public final void execute(String user) { // 流れを固定
audit("start", user);
doExecute(user); // 差し替えポイント
audit("end", user);
}
protected abstract void doExecute(String user);
private void audit(String phase, String user) { System.out.println("[AUDIT] " + phase + " by " + user); }
}
final class TransferService extends SecureService {
@Override protected void doExecute(String user) { System.out.println("transfer by " + user); }
}
Javaこの形だと、監査を抜き忘れる事故を親で防げます。拡張は抽象メソッドの実装だけに集中できます。
例題で身につける
例 1: ストレージの差し替え
親が「保存」の契約を定め、保存先の違い(メモリ・ファイルなど)を子で実装します。
abstract class Storage {
public final void put(String key, String value) {
if (key == null || key.isBlank()) throw new IllegalArgumentException("key");
if (value == null) throw new IllegalArgumentException("value");
doPut(key.trim(), value);
}
protected abstract void doPut(String key, String value);
}
final class MemoryStorage extends Storage {
private final java.util.Map<String, String> map = new java.util.HashMap<>();
@Override protected void doPut(String key, String value) { map.put(key, value); }
}
final class FileStorage extends Storage {
@Override protected void doPut(String key, String value) { System.out.println("write: " + key + "=" + value); }
}
Java呼び出し側は Storage 型だけを知っていればよく、実体に応じて保存方法が切り替わります。
例 2: フォーマッタの段階化
親が基本の整形を持ち、仕上げの規則だけ子で差し替えます。
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つまずきポイントと回避(重要)
コンストラクタで“仮想呼び出し”をしない
抽象メソッドはオーバーライドされる前提のメソッドです。親のコンストラクタ内で、オーバーライド可能なメソッド(抽象または非 final)を呼ぶと、子のフィールドが未初期化のまま参照される危険があります。初期化は「親の完成→子の完成」の順序を崩さない設計にし、コンストラクタ内で仮想呼び出しを避けてください。
契約の曖昧さを残さない
抽象メソッドの引数の前提、戻り値の意味、例外方針を親で明文化し、共通の検証は親で行います。子が自由に実装しても整合性が保たれるよう、差し替えポイントは小さく限定します。
インターフェースとの使い分け
「状態(フィールド)や共通実装を親で持ちたい」なら抽象クラス。「軽い契約だけを複数組み合わせたい」ならインターフェース。インターフェースのメソッドも抽象ですが、default を使えば簡易実装を提供できます。両者を併用し、抽象クラスは1系統に絞るのが安定します。
仕上げのアドバイス(重要部分のまとめ)
抽象メソッドは「型としての約束を決め、差し替えるべき処理だけを子に任せる」ための要です。親は流れ・前処理・検証を持ち、抽象メソッドで拡張点を明確に切る。コンストラクタで仮想呼び出しを避け、契約の意味を明文化し、差し替え範囲は最小に保つ——この型を守れば、拡張しやすく壊れにくい設計が手に入ります。
