Java | オブジェクト指向:final メソッド

Java Java
スポンサーリンク

final メソッドとは何か

final メソッドは「子クラスからオーバーライド(上書き)できないメソッド」です。継承関係にあっても、その振る舞いを固定し、契約を壊されないようにします。親が守るべき安全策(前処理・検証・監査)や処理の骨格を final にすると、拡張しても最低限の品質が機械的に守られます。


なぜ final にするのか(重要ポイントの深掘り)

契約と安全策を壊させない

子が上書きできると、親が保証したい不変条件(インバリアント)や監査・検証が抜ける危険があります。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);
    }
}
class TransferService extends SecureService {
    @Override protected void doExecute(String user) { System.out.println("transfer by " + user); }
}
Java

壊れやすい基底クラス問題の抑制

親の流れ(呼び順)や必須処理を final で固定すると、子の不注意なオーバーライドで骨格が崩れることを防げます。拡張ポイントは小さく限定し、骨格は変えられない設計が安全です。

テンプレートメソッドと相性が良い

「流れは親で固定、差し替えポイントだけ子に開く」というテンプレートメソッドで、固定すべきメソッドを final にするのが定石です。拡張の自由度と安全性を両立できます。


使いどころと設計の型

骨格(フロー)を固定し、局所のフックを開く

処理の順序や必須前処理は final で固定し、細部は protected/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);           // フック
}
Java

検証付きの操作を必ず通す窓口にする

状態変更や副作用のある操作は final にして、内部の検証ルールを抜け道なく適用します。

class BankAccount {
    private int balance;
    public BankAccount(int initial) {
        if (initial < 0) throw new IllegalArgumentException();
        this.balance = initial;
    }
    public final boolean withdraw(int amount) {          // 検証付きの窓口を固定
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }
    public final void deposit(int amount) {
        if (amount <= 0) throw new IllegalArgumentException();
        balance += amount;
    }
    // 差し替えたい拡張は別のフックに分離して公開する
}
Java

final と他の性質の関係

private・static・final の違い

  • private メソッドは子から見えないため、結果的にオーバーライド不可(ただし「隠蔽」で同名別メソッドは作れてしまうので、骨格固定には public/protected final が有効)。
  • static メソッドはクラスメソッドで、オーバーライドではなく隠蔽(hiding)。動的に切り替わらないため、骨格固定の意味では final と似た効果はあるが、インスタンスの文脈では使えない。
  • final メソッドは子から見えていても上書けないため、契約を公開しつつ固定できる。

@Override と相補性

final にしたメソッドには @Override は付けられません(上書き不可)。一方、子が上書くべきフックには @Override を徹底して、意図せぬオーバーロードを防ぎます。固定するものと差し替えるものを明確に区別するのが設計の要です。


つまずきやすい落とし穴(重要ポイントの深掘り)

なんでも final にすると拡張性が死ぬ

骨格や検証窓口のように「変えてほしくないところ」だけ final にします。細部のアルゴリズムや出力フォーマットなど、差し替え可能であるべき箇所はフックとして開くバランスが重要です。

コンストラクタから“仮想呼び出し”をすると危険

初期化中にオーバーライド可能メソッド(final でない)を呼ぶと、下位で未初期化のフィールドを参照する事故が起きます。骨格を final にするなら、コンストラクタでは仮想呼び出しを避け、ファクトリ/ビルダーで段階化するのが安全です。

テストで差し替えにくくなる

final によってモックの「継承差し替え」は使えません。差し替えたい部分はインターフェースに切り出し、委譲で注入できる形にするとテスト容易性を保てます。

interface Normalizer { String apply(String s); }
final class TrimNormalizer implements Normalizer {
    @Override public String apply(String s) { return s == null ? "" : s.trim(); }
}
final class Service {
    private final Normalizer n;                   // 差し替えはここで
    Service(Normalizer n) { this.n = n; }
    public final String run(String s) {           // 骨格は final
        return n.apply(s);
    }
}
Java

例題で身につける

例 1: ログと監査の枠組みを final で固定

abstract class LoggerService {
    public final void process(String task) {              // 枠組み固定
        System.out.println("[BEGIN] " + task);
        doProcess(task);                                  // 差し替えポイント
        System.out.println("[END] " + task);
    }
    protected abstract void doProcess(String task);
}
class EmailService extends LoggerService {
    @Override protected void doProcess(String task) { System.out.println("send email: " + task); }
}
Java

例 2: 値オブジェクトの表示と比較の一貫性を守る

final class ProductCode {
    private final String value;
    ProductCode(String raw) {
        var v = raw == null ? "" : raw.trim().toUpperCase();
        if (v.isBlank()) throw new IllegalArgumentException("code");
        this.value = v;
    }
    public final String value() { return value; }         // 仕様固定(オーバーライド不要)
    @Override public final String toString() { return value; } // 表示の一貫性も固定
    @Override public final int hashCode() { return value.hashCode(); }
    @Override public final boolean equals(Object o) {
        return (o instanceof ProductCode pc) && value.equals(pc.value);
    }
}
Java

例 3: 正規化ルールを final にして拡張は別フック

abstract class NameFormatter {
    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);      // 差し替え必須
}
class FancyFormatter extends NameFormatter {
    @Override protected String postprocess(String s) { return s.toUpperCase(); }
}
Java

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

final メソッドは「変えてはいけない窓口・骨格・安全策」を固定し、継承で壊されないようにするための鍵です。固定すべき所だけを final にし、差し替えたい所は小さなフックとして開く。コンストラクタで仮想呼び出しを避け、テスト差し替えはインターフェース+委譲で担保する——この線引きを徹底すると、拡張しやすく壊れにくい継承が実現します。

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