Java | オブジェクト指向:メソッドのオーバーライド

Java Java
スポンサーリンク

何が「オーバーライド」か

メソッドのオーバーライドは、親クラスが持つメソッドの「振る舞いを、子クラスで置き換える」ことです。継承関係にあるとき、同じメソッド名・同じ引数(シグネチャ)を子クラスで再定義すると、実行時には「実体の型」に応じたメソッドが選ばれます。これが多態性(ポリモーフィズム)で、拡張先ごとに振る舞いを差し替える設計が簡潔になります。


基本ルールと @Override

必須の一致と推奨アノテーション

オーバーライドは「メソッド名・引数の並び・戻り値の互換性」が親と合致している必要があります。Java では @Override を付けておくと、間違って「別メソッド(オーバーロード)」になってしまう事故をコンパイラが防いでくれます。

abstract class Shape {
    abstract double area();
}
final class Rect extends Shape {
    @Override double area() { return w * h; }    // 正しいオーバーライド
    Rect(double w, double h) { this.w = w; this.h = h; }
    private final double w, h;
}
final class Circle extends Shape {
    @Override double area() { return Math.PI * r * r; }
    Circle(double r) { this.r = r; }
    private final double r;
}
Java

@Deprecated な親メソッドを置き換える、インターフェースの実装を確実にしたい、という場面でも @Override は強力なガードになります。


動的ディスパッチとポリモーフィズム(重要ポイントの深掘り)

参照型ではなく「実体の型」で決まる

メソッド呼び出しは、変数の参照型ではなく、その時持っている実体の型で決まります。これにより「共通の型(親)で扱い、具体型に応じて振る舞いが切り替わる」柔軟な設計が可能です。

Shape s = new Rect(3, 4);
System.out.println(s.area());      // Rect の実装が呼ばれる
s = new Circle(2);
System.out.println(s.area());      // Circle の実装に切り替わる
Java

静的メソッドやフィールド参照は動的ディスパッチに参加しないため、オーバーライドの対象にはなりません。インスタンスメソッドが多態性の中心です。


戻り値・アクセス修飾子・例外の互換性

戻り値は「共変戻り値」が許される

戻り値は親より「より具体的な型(サブタイプ)」に狭めることができます。これを共変戻り値と呼び、使い勝手が向上します。

class Animal { }
class Cat extends Animal { }
class Factory {
    Animal create() { return new Animal(); }
}
class CatFactory extends Factory {
    @Override Cat create() { return new Cat(); } // 共変戻り値(OK)
}
Java

アクセス修飾子は「広げるのはOK、狭めるのはNG」

親が public のメソッドを子で protected や private に狭めることはできません。公開契約を壊すためです。逆に、package-private を public に広げることは可能です。

class Base { void greet() {} }          // package-private
class Sub extends Base {
    @Override public void greet() {}    // より広い公開(OK)
}
Java

例外は「より少なく・より具体的」に

throws のチェック例外は、親の宣言より「狭い例外」を投げるのは許されますが、広げることはできません。非チェック例外(RuntimeException系)は宣言外でも投げられますが、契約の明確化を心がけましょう。


オーバーライドできないものと super

final・static・private は対象外

final メソッドは上書き不可。static はクラスメソッドであり、オーバーライドではなく「隠蔽(hiding)」になって動的には切り替わりません。private はクラス外から見えないため、そもそもオーバーライド不能です。

class Base {
    final void lock() {}        // 上書き不可
    static void util() {}       // 隠蔽はできるが動的切替なし
    private void internal() {}  // 子から見えない
}
Java

super の使いどころ

親の処理を拡張する場合、子のメソッド内から super を呼んで「前後や途中に追加処理」を差し込めます。テンプレートメソッドやフックの実装で有用です。

class Base {
    void render() { System.out.println("header"); }
}
class Sub extends Base {
    @Override void render() {
        super.render();                       // 親の前処理
        System.out.println("body");           // 追加
    }
}
Java

オーバーロードとの違いと混同防止(重要ポイントの深掘り)

引数が違えば「別メソッド(オーバーロード)」

オーバーライドは「親と同じシグネチャ」を置き換えます。引数の型や個数が違うと、それはオーバーロード(同名別メソッド)です。@Override を付けていれば、意図せずオーバーロードしてしまうミスを防げます。

class Base { void send(String s) {} }
class Sub extends Base {
    // @Override を付けるとコンパイル時に誤りを検出できる
    void send(Object o) {}   // これはオーバーロード。オーバーライドではない
}
Java

設計パターンと実践例

テンプレートメソッドで差し替えポイントを作る

共通の処理枠組みは親で final にし、差し替えたい場所だけ protected/abstract にして子クラスでオーバーライドします。拡張性と安全性のバランスが良い定番です。

abstract class Importer {
    public final void run(String path) {
        var data = load(path);
        var normalized = normalize(data);
        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("save:" + data); }
}
Java

equals/hashCode/toString のオーバーライドと同様に「契約」を守る

オーバーライドは「外部が期待する契約」を壊さないことが大前提です。引数の前提、戻り値の意味、例外方針は親と互換にし、必要な拡張はドキュメントとテストで明確化します。


つまずきやすいポイントと回避

オーバーライドしたつもりが引数違いでオーバーロードになり、実行時に親のメソッドが呼ばれてしまう混乱が典型です。必ず @Override を付けて防ぎます。親の公開範囲より狭くする、チェック例外を広げる、といった互換性違反はコンパイルエラーや破壊的変更につながります。戻り値の共変は便利ですが、「より広い型」に戻してしまうと互換性を逸脱するため注意してください。final/static/private はそもそもオーバーライド対象外で、動的切り替えが期待できません。


例題で理解を固める

abstract class Notifier {
    public final void sendAll(java.util.List<String> targets) {
        for (var t : targets) sendTo(t);
    }
    protected abstract void sendTo(String target); // 差し替えポイント
}
final class EmailNotifier extends Notifier {
    @Override protected void sendTo(String target) {
        System.out.println("Email -> " + target);
    }
}
final class SmsNotifier extends Notifier {
    @Override protected void sendTo(String target) {
        System.out.println("SMS -> " + target);
    }
}

Notifier n = new EmailNotifier();
n.sendAll(java.util.List.of("taro@example.com", "hanako@example.com"));
// 実体に応じて sendTo が切り替わる(動的ディスパッチ)
Java

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

オーバーライドは「親の契約を守りつつ、子で振る舞いを差し替える」技術です。同じシグネチャで再定義し、@Override で意図を固定。戻り値は共変で狭められ、アクセスは広げられるが、例外は広げない。final/static/private は対象外で、動的ディスパッチはインスタンスメソッドだけ。テンプレートメソッドで差し替え点を設計すると、拡張しやすく壊れにくいコードになります。

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