SJ blog
beginner
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

オーバーライドの細かい制約 — アクセス修飾子・戻り値・例外・staticハイディング

オーバーライド時にアクセス修飾子は緩くのみ変更可能な理由、共変戻り値型とは何か、親よりも広いchecked例外をthrowできない理由、staticメソッドのハイディングを解説。

一言結論

オーバーライドの制約はリスコフの置換原則に基づく。親クラス型として扱われたときに矛盾が生じないよう、アクセスは広げる方向のみ、戻り値は子クラス方向のみ、例外は狭める方向のみ許可される。

オーバーライドの基本と「4つの制約」

子クラスで親クラスのメソッドを上書きするのがオーバーライドだ:

class Animal {
    public String speak() { return "..."; }
}

class Dog extends Animal {
    @Override
    public String speak() { return "ワン"; }  // オーバーライド
}

このとき、4つの細かい制約がある:

  1. アクセス修飾子は「広げる」方向のみ変更可能
  2. 戻り値の型は同じか「より狭い型(子クラス)」のみ
  3. throws宣言できるchecked例外は「より狭い」ものだけ
  4. staticメソッドはオーバーライドできない(ハイディング)

それぞれなぜそうなのかを見ていく。

制約1:アクセス修飾子は「緩く」する方向のみ

class Parent {
    public void hello() { }
    protected void hi() { }
}

class Child extends Parent {
    @Override
    protected void hello() { }  // ❌ publicをprotectedに「厳しく」するのはNG

    @Override
    public void hi() { }        // ✅ protectedをpublicに「緩く」するのはOK
}

なぜか?

Javaではポリモーフィズムにより、親クラス型の変数で子クラスのオブジェクトを扱える

Parent p = new Child();  // 親クラス型でChildを持つ
p.hello();               // Parentとして呼ぶ

Parentとして呼んでいるので、Parentpublicのメソッドは当然呼べるはずだ。でもそこにChildのオブジェクトが入っていて、Childprotectedに制限されていたら:

// 呼び出し元は「Parentのpublicなhello()を呼ぶ」つもり
// でも実際のオブジェクトはChildで、そのhello()はprotected
// → Parentとして呼べるはずのものが呼べなくなる。矛盾!

親クラスとして扱われたときに使えるものが使えなくなってはいけない」という原則があり、これがリスコフの置換原則(LSP) の一部だ。アクセスを狭めることはこの原則に違反する。

制約2:戻り値の型は「共変戻り値型」

class Animal {
    public Animal create() { return new Animal(); }
}

class Dog extends Animal {
    @Override
    public Dog create() { return new Dog(); }  // ✅ DogはAnimalの子クラス
}

class Cat extends Animal {
    @Override
    public Object create() { return new Cat(); }  // ❌ ObjectはAnimalの親クラスなのでNG
}

戻り値は同じ型か、その子クラス(より具体的な型)のみ許可される。これを共変戻り値型(Covariant Return Type) という。

なぜか? 同じくリスコフの置換原則だ:

Animal a = new Dog();
Animal result = a.create();  // Animal型として受け取る

Animalcreate()Animalを返す」と呼び出し元は思っている。実際に返ってくるのがDog(Animalの子クラス)なら問題ない。DogAnimalとして扱えるから。

でもObjectが返ってきたら?ObjectAnimalとして扱えない(Animalのメソッドを持っていない)。呼び出し元の期待を裏切ることになる。

制約3:throws宣言できるchecked例外は「より狭く」

class Parent {
    public void method() throws IOException { }
}

class Child extends Parent {
    @Override
    public void method() throws FileNotFoundException { }  // ✅ IOExceptionの子クラス

    @Override
    public void method() throws Exception { }              // ❌ IOExceptionより広い

    @Override
    public void method() { }                               // ✅ 例外なしもOK
}

なぜか? 呼び出し元は親クラスの宣言を見てtry-catchを書く

Parent p = new Child();
try {
    p.method();
} catch (IOException e) {  // ← IOExceptionしかcatchしていない
    // ...
}

Childmethod()Exceptionを投げてきたら、IOExceptionのcatchでは捕まえられない。呼び出し元のコードが機能しなくなる。

「子クラスが親より広い例外を投げる」ことは、親クラス型で扱うコードの安全性を壊す。だからより狭い(または同じ)例外しか宣言できない。

ただしunchecked例外(RuntimeException系)は制約の対象外だ:

class Child extends Parent {
    @Override
    public void method() throws NullPointerException { }  // ✅ 制約なし(unchecked)
}

unchecked例外はどのメソッドでも発生しうるので、宣言の有無が呼び出し元のcatch設計に影響しない。

制約4:staticメソッドは「ハイディング」

staticメソッドを子クラスで同名で定義しても、**オーバーライドではなくハイディング(隠蔽)**になる:

class Parent {
    public static void greet() { System.out.println("Parent"); }
}

class Child extends Parent {
    public static void greet() { System.out.println("Child"); }
}

Parent p = new Child();
p.greet();  // "Parent"(!)

オーバーライドならChildの実装が呼ばれるはずだが、staticのハイディングでは変数の宣言型Parent)で呼ばれるメソッドが決まる。ポリモーフィズムが効かない。

なぜか?

オーバーライドは「実行時にオブジェクトの実際の型を見てメソッドを選ぶ(動的ディスパッチ)」という仕組みだ。でもstaticメソッドはインスタンスではなくクラスに属している。クラスは実行時のオブジェクトとは独立して存在するので、「実行時のオブジェクトの型を見る」という仕組みが適用されない。

@Overrideアノテーションをstaticメソッドにつけるとコンパイルエラーになる:

class Child extends Parent {
    @Override  // ❌ エラー: staticメソッドはオーバーライドできない
    public static void greet() { System.out.println("Child"); }
}

@Overrideはコンパイラに「これはオーバーライドであることを検証せよ」と指示するアノテーションだ。staticメソッドにはオーバーライドの概念がないのでエラーになる。このアノテーションがあるおかげで「オーバーライドしたつもりが実は違った」というバグを防げる。

オーバーライドとオーバーロードの混同

class Animal {
    public void eat(String food) { }
}

class Dog extends Animal {
    public void eat(int amount) { }  // ← オーバーロード(引数の型が違う)
}

引数の型・数・順序が違うとオーバーライドではなくオーバーロードになる。@Overrideをつけておけば、意図せずオーバーロードしていた場合にコンパイラが教えてくれる。

class Dog extends Animal {
    @Override
    public void eat(int amount) { }  // ❌ コンパイルエラー: これはオーバーライドではない
}

まとめ

制約ルール理由
アクセス修飾子緩くのみ変更可能親クラス型から呼べるものが子クラスで呼べなくなってはいけない
戻り値の型同じか子クラス(共変)のみ親の戻り値として受け取るコードが機能しなくなってはいけない
checked例外同じか狭いもののみ親のcatchで捕まえられない例外が飛んでくることを防ぐ
staticオーバーライド不可(ハイディング)staticは動的ディスパッチの対象外。宣言型で呼ぶメソッドが決まる

4つの制約すべてに共通しているのは「親クラス型として扱われたときに矛盾が生じてはいけない」という一本の原則だ。個別に暗記するより、この原則から導けるようになると忘れにくい。