メソッドのオーバーライド

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 親の同名メソッドを子で 同じシグネチャ で再定義すると override
  • @Override アノテーションを付ける ─ タイプミス検出に有効
  • 戻り型は共変 (covariant) ─ より具体的な型を返してもOK
  • アクセス修飾子は 同じか緩める 方向のみ (protectedpublic はOK、逆はエラー)

メソッドのオーバーライド とは

親クラスのメソッドを子クラスで上書きするオーバーライドの仕組みを学び、Shape を継承した Circle で area() を再定義してみよう。

親の振る舞いを上書きする オーバーライド

継承を勉強し始めると、必ず出会うのが メソッドのオーバーライド です。これは、親クラスで定義されたメソッドを、子クラスで同じシグネチャのまま「中身だけ別物」に書き換える仕組みです。たとえば Shape という親クラスが area() というメソッドを持っているとして、子クラスの Circle では「半径から円の面積を計算する」、Square では「一辺から四角形の面積を計算する」というように、子ごとに違う実装を提供できます。

親クラスは「こういう振る舞いが必要だよ」という共通の約束ごとを決め、子クラスは「その約束をどう実現するかは自分で決める」という分業の考え方です。これがオブジェクト指向の中核である ポリモーフィズム (多態性) の入口です。

外から呼ぶ側は Shape s = new Circle(...) のように親の型で受け取り、s.area() と呼ぶだけで、中身の Circle 版が走ってくれる。呼び出し側は「何が来ても area() で面積が取れる」という安心感を持てるので、コードがとても柔軟になります。本章では、この オーバーライド の文法と落とし穴を、@Override アノテーションや動的ディスパッチの仕組みと合わせて掘り下げていきます。

文法 ─ 子クラスで同じシグネチャを書き直す

オーバーライド の文法は単純です。子クラスで、親クラスとまったく同じ 戻り値の型メソッド名引数リスト を持つメソッドを定義するだけです。

Java

class Shape { public int area() { return 0; } } class Circle extends Shape { private int radius; public Circle(int radius) { this.radius = radius; } @Override public int area() { return (int) (radius * radius * 3.14); } }

Circle 側の area() は、シグネチャだけ見れば Shape.area() と同じです。違うのは中身だけ。これだけで、Circle のインスタンスから area() を呼ぶと、親のではなく Circle の実装が選ばれます。

ここで重要なのが、シグネチャを完全に一致させることです。引数を 1 つでも増やしたり、引数の型を変えたりすると、Java はそれを「別のメソッド」と見なし、オーバーライド ではなく オーバーロード 扱いにしてしまいます。親の area() はそのまま生き残り、子の area(int x) は単に追加された別メソッドになるという、本人が気付きづらいバグの種になります。

@Override アノテーションは「保険」として必ず付ける

メソッドの上に書く @Override は、Java のコンパイラへ「これは親のメソッドを上書きしているつもりだよ」と伝えるためのアノテーションです。書いても書かなくても動作は同じですが、書くべき です。理由は次のとおりです。

  • もしシグネチャを間違えて (たとえば aera とタイポしたり、引数を long にしてしまったり) 親に対応するメソッドが見つからない場合、@Override が付いていればコンパイル時にエラーになる
  • 付けていないと、ただの「似た名前の別メソッド」としてコンパイルが通ってしまい、実行時に「呼ばれてるはずなのに動いていない」という不可解な現象になる
  • IDE やコードレビューで「親のメソッドを意識して書いているコード」だと一目で分かり、読み手の負担が減る

言ってみれば @Overrideタイポ防止の保険 であり、開発体験を底上げする小さな投資です。「動くから付けなくていい」ではなく、オーバーライド するつもりのメソッドには必ず付ける、と覚えておきましょう。

Java

@Override public int area() { return (int) (radius * radius * 3.14); }

似た仕組みに @Deprecated@SuppressWarnings がありますが、これらと違って @Override はコンパイル時の自動検査をオンにするためのアノテーションで、間違いに気付かせてくれる役目を持ちます。

動的ディスパッチ ─ 実行時の型で呼び先が決まる

オーバーライド の真価は、動的ディスパッチ と呼ばれる仕組みで発揮されます。これは「親の型で参照していても、実際のインスタンスがどの子クラスかで呼ばれるメソッドが決まる」という挙動です。

Java

Shape s = new Circle(10); int a = s.area(); // 314 が返る (Circle.area が呼ばれる)

変数 s の型は Shape ですが、実体は Circle です。s.area() を呼ぶと、コンパイル時には「Shape.area() を呼ぶつもり」と書いてあっても、実行時に「中身が Circle だから Circle.area() を使おう」と JVM が判断します。これが 動的ディスパッチ です。

diagram (will load when visible)

この仕組みのおかげで、配列やリストに Shape を入れて回すコードは、中身が Circle でも Square でも Triangle でも、同じ area() 呼び出し 1 行で済みます。

Java

Shape[] shapes = { new Circle(10), new Square(5) }; int total = 0; for (Shape sh : shapes) { total += sh.area(); }

Java の インスタンスメソッド は標準でこの 動的ディスパッチ の対象になります。一方で static メソッドprivate メソッドfinal メソッド は対象外で、コンパイル時の型でそのまま呼び先が確定します。後ほど「よくある間違い」で static の落とし穴を扱います。

オーバーライドのルール

オーバーライド には、Java の仕様で決まった守るべきルールがあります。代表的なものは次のとおりです。

  • メソッド名と引数リストは完全に一致 ─ 違うと別メソッド扱い (オーバーロード) になる
  • 戻り値の型 は親と同じか、共変戻り値 として親の戻り値型のサブクラスにできる (たとえば親が Number を返すなら子は Integer を返してよい)
  • アクセス修飾子 は親より厳しくしてはいけない。親が public なら子は public のみ、親が protected なら子は protectedpublic まで広げられる
  • throws は親より広い (チェック例外を増やす) ことはできない。同じか、より狭い範囲のみ許される
  • 親が privatestaticfinal のメソッドは オーバーライド できない
diagram (will load when visible)

アクセス修飾子のルールは、「親の型で受け取った変数経由で呼んだら成功するのに、子の型ならアクセス不能」という矛盾を防ぐためのものです。Liskov の置換原則 (子クラスは親クラスの代わりに使えるべき) を Java の文法レベルで保証している、と考えるとしっくりきます。

よくある間違い

初学者が オーバーライド で踏みやすい罠を 3 つ挙げます。事前に知っているだけで、デバッグの時間が大幅に減ります。

  • @Override を忘れてオーバーロードになる ─ 親が area() なのに子で area(int x) と書いてしまうと、別メソッドが追加されただけで親の area() はそのまま生きてしまいます。Shape s = new Circle(10); s.area(); を呼んでも親の 0 が返ってきて「なんで動かないんだ?」と悩むパターンです。 @Override を付けておけばコンパイラが「親に一致するメソッドがない」と弾いてくれます
  • アクセス修飾子を絞ってしまう ─ 親の public int area() を子で protected int area() と書くと、コンパイルエラーになります。これは前述のルールどおりで、「子は親より広く、または同じ」可視性しか許されません。逆向き (protectedpublic) はオッケーです
  • static メソッドを「オーバーライド」しているつもり ─ 親と子で同名の static メソッドを書くと、文法上は通りますがこれは オーバーライド ではなく 隠蔽 (hiding) と呼ばれる別の現象です。動的ディスパッチ の対象外で、呼び先はコンパイル時の型で決まります。 Shape.foo()Circle.foo() がそれぞれ独立に存在し、Shape s = new Circle(); s.foo(); は親の foo を呼んでしまいます。 static メソッドは オーバーライド できないと覚えておきましょう

もうひとつ、戻り値の型を勝手に変えてしまうケースも多いです。親が int area() で子を double area() にすると、これも「別シグネチャ」とみなされてコンパイルエラーになります。 共変戻り値 で許されるのはあくまでサブクラスへの絞り込みだけです。

エラーメッセージで method does not override or implement a method from a supertype が出たら、ほぼ間違いなく @Override を付けたメソッドのシグネチャがどこか親とずれています。スペル ・ 引数 ・ 戻り値の順でチェックしましょう。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • toString() のカスタム表示 ─ ログやデバッグで読みやすく表示
  • equals() / hashCode() の整合性 ─ Map / Set キーで使う際の必須セット
  • テンプレートメソッドの拡張ポイント ─ 親が骨格を持ち、子が具体実装
  • Comparable.compareTo の独自実装 ─ ソート順を業務ロジックに合わせる

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • @Override を付け忘れている ─ シグネチャミスでも黙って通る危険があり、付けるルールにしているチームが多い
  • equals を override したのに hashCode 未対応 ─ HashMap が壊れる定番バグ。両方セットで実装
  • final メソッドを override しようとしている ─ コンパイルエラー、設計上 override 不可の意思表示
  • 戻り型を変える ─ プリミティブとラッパーの混在 (int → Integer) は不可、参照型なら共変OK

パフォーマンス考慮事項

  • 仮想呼び出し (vtable lookup) ─ JIT がほぼインライン化、実用上のコストはゼロ
  • equals の高速化 ─ 比較順を == チェック → null → 型 → フィールドの順に書く
  • hashCode のキャッシュ ─ イミュータブルクラスなら計算結果をキャッシュ可能 (例 String.hash)
  • 深い継承での override ─ Class Hierarchy が深いと初回呼び出しの解決コストが少し増える
この章のポイント

ここまでの要点 override は同じシグネチャでメソッドを再定義。@Override を付け、equalshashCode はセット。アクセスは緩める方向のみ。

やってみよう

今回の課題は、Shape を継承した Circle クラスを作り、area()オーバーライド するパターンの練習です。やることは次のとおりです。

  1. 親クラス Shape を定義し、public int area() を持たせる (中身は 0 を返すだけで OK)
  2. 子クラス Circle extends Shape を作り、int radius フィールドとコンストラクタを用意する
  3. Circle 側で @Override public int area() を実装し、(int)(radius * radius * 3.14) を返す
  4. Solution.circleArea(int radius) の中で Shape shape = new Circle(radius); のように親の型で受け取り、shape.area() の戻り値をそのまま return する

テストでは circleArea(1)3circleArea(10)314circleArea(0)0 を返すことを確認しています。 int へのキャストで小数点以下が切り捨てられる点に注意してください。 1 * 1 * 3.143.14 なので、 (int) キャストで 3 になります。

慣れてきたら、Square クラスを追加して area()side * side で実装し、Shape[] 配列にまとめて for ループで面積の合計を求めてみるとよい練習になります。 動的ディスパッチ を体感できる課題なので、ぜひ自分の手で書いて試してみてください。次のレッスンでは super キーワードや 抽象クラス を扱い、オーバーライド をさらに整理した形で使えるようにしていきます。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は abstract で抽象クラス で、abstract で抽象クラス を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. @Override の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. @Override とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 親クラス Shape を用意し、public int area() を定義すること (中身は 0 でよい)
  2. 子クラス Circle extends Shape を作り、@Override を付けて area() を再定義し、(int)(radius * radius * 3.14) を返すこと
  3. circleArea の中で Shape shape = new Circle(radius); のように親の型で受けて shape.area() の値を返すこと (動的ディスパッチで Circle.area が呼ばれる)

入出力例

test-cases.txt

circleArea(1)3 circleArea(10)314 circleArea(0)0 circleArea(5)78

ヒント

main.java
main.java
学習モード

メモ

メソッドのオーバーライド

⌘S で保存