メソッドのオーバーライド
このレッスンで分かること
- 親の同名メソッドを子で 同じシグネチャ で再定義すると override
@Overrideアノテーションを付ける ─ タイプミス検出に有効- 戻り型は共変 (
covariant) ─ より具体的な型を返してもOK- アクセス修飾子は 同じか緩める 方向のみ (
protected→publicは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 が判断します。これが 動的ディスパッチ です。
この仕組みのおかげで、配列やリストに 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なら子はprotectedかpublicまで広げられる throws句 は親より広い (チェック例外を増やす) ことはできない。同じか、より狭い範囲のみ許される- 親が
private・static・finalのメソッドはオーバーライドできない
アクセス修飾子のルールは、「親の型で受け取った変数経由で呼んだら成功するのに、子の型ならアクセス不能」という矛盾を防ぐためのものです。
Liskov の置換原則(子クラスは親クラスの代わりに使えるべき) を Java の文法レベルで保証している、と考えるとしっくりきます。
よくある間違い
初学者が オーバーライド で踏みやすい罠を 3 つ挙げます。事前に知っているだけで、デバッグの時間が大幅に減ります。
@Overrideを忘れてオーバーロードになる ─ 親がarea()なのに子でarea(int x)と書いてしまうと、別メソッドが追加されただけで親のarea()はそのまま生きてしまいます。Shape s = new Circle(10); s.area();を呼んでも親の0が返ってきて「なんで動かないんだ?」と悩むパターンです。@Overrideを付けておけばコンパイラが「親に一致するメソッドがない」と弾いてくれます- アクセス修飾子を絞ってしまう ─ 親の
public int area()を子でprotected int area()と書くと、コンパイルエラーになります。これは前述のルールどおりで、「子は親より広く、または同じ」可視性しか許されません。逆向き (protected→public) はオッケーです 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 を付け、equals と hashCode はセット。アクセスは緩める方向のみ。
やってみよう
今回の課題は、Shape を継承した Circle クラスを作り、area() を オーバーライド するパターンの練習です。やることは次のとおりです。
- 親クラス
Shapeを定義し、public int area()を持たせる (中身は0を返すだけで OK) - 子クラス
Circle extends Shapeを作り、int radiusフィールドとコンストラクタを用意する Circle側で@Override public int area()を実装し、(int)(radius * radius * 3.14)を返すSolution.circleArea(int radius)の中でShape shape = new Circle(radius);のように親の型で受け取り、shape.area()の戻り値をそのままreturnする
テストでは circleArea(1) が 3 、 circleArea(10) が 314 、 circleArea(0) が 0 を返すことを確認しています。 int へのキャストで小数点以下が切り捨てられる点に注意してください。 1 * 1 * 3.14 は 3.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 つができることを確認しましょう。
- @Override の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. @Override とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- 親クラス
Shapeを用意し、public int area()を定義すること (中身は0でよい) - 子クラス
Circle extends Shapeを作り、@Overrideを付けてarea()を再定義し、(int)(radius * radius * 3.14)を返すこと 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