中級 70分 Lesson 4

継承・オーバーライド・抽象クラス・ポリモーフィズム

extends・super・オーバーライドルール(共変・例外・hiding)・抽象クラス・ポリモーフィズムの実行時型決定・finalを完全解説

Java Java Silver SE21 継承 ポリモーフィズム

Chapter 04 ─ 継承・オーバーライド・抽象クラス・ポリモーフィズム

この章は OOP の核心。 Silver 試験の 33% を占める OOP ドメインでは、オーバーライドのルール・ポリモーフィズムのメソッドとフィールドの違い・抽象クラスとインターフェースの比較が頻出。理解できれば試験の得点源になる。


4-1. 継承の基本

継承とは「親から子へ能力を引き継ぐ」仕組み

extends を使って親クラス(スーパークラス)を指定すると、子クラス(サブクラス)は親のフィールドとメソッドをそのまま使える。さらに子クラス独自のフィールドやメソッドを追加できる。

// 親クラス
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " が食事をする");
    }

    public void breathe() {
        System.out.println(name + " が呼吸する");
    }
}

// 子クラス(Animal を継承)
public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name);         // 親のコンストラクタを呼ぶ(最初の行)
        this.breed = breed;
    }

    // 親から eat(), breathe() を継承して使える
    // 独自メソッドを追加できる
    public void bark() {
        System.out.println(name + "(" + breed + ")がワンワン");
    }
}
Dog d = new Dog("ポチ", "柴犬");
d.eat();      // 親から継承したメソッド
d.breathe();  // 親から継承したメソッド
d.bark();     // Dog 独自のメソッド

継承の is-a 関係

「Dog is an Animal(犬は動物だ)」という関係があるときに継承を使う。「Dog has a Leash(犬はリードを持っている)」という関係は継承ではなくコンポジション(フィールドとして持つ)を使う。

継承を間違えて使うと設計が崩れる。「Square extends Rectangle(正方形は長方形だ)」は数学的には正しいが、「Rectangle.setWidth(5) を正方形に呼んだら width だけ変わって正方形でなくなる」という問題が起きる(リスコフの置換原則違反)。試験ではここまで問われないが、「is-a なら継承」という判断基準は覚えておく価値がある。

Java の継承は「単一継承」

クラスは extends できるのは1つのクラスだけ。2つ以上のクラスを同時に継承する「多重継承」はできない。

class A { }
class B { }
class C extends A { }         // ✅ OK
class D extends A, B { }      // ❌ コンパイルエラー(多重継承は不可)

多重継承が禁止されている理由は「ダイヤモンド問題」を防ぐため。AB に同名のメソッドがあった場合、C はどちらを使うべきか判断できなくなる。インターフェースの default メソッドが複数衝突した場合は実装クラスがオーバーライドすることで解決する(Chapter 05)。

継承チェーン

継承は連鎖できる(A → B → C のように多段階)。

class A {
    void methodA() { System.out.println("A"); }
}
class B extends A {
    void methodB() { System.out.println("B"); }
}
class C extends B {
    void methodC() { System.out.println("C"); }
}

C c = new C();
c.methodA();  // A から引き継ぎ
c.methodB();  // B から引き継ぎ
c.methodC();  // C 自身

C のオブジェクトには A, B, C のメソッドがすべて含まれる。

Object クラス ─「すべての親」

Java では extends を書かなければ、暗黙的に java.lang.Object を継承する。つまりすべてのクラスは Object のサブクラス。

class Foo { }
// ↑ 内部的には class Foo extends Object { } と同じ

これにより toString(), equals(), hashCode() などがすべてのオブジェクトで使える(後述)。

super でできること

super は親クラスへの参照として2つの使い方がある。

public class Dog extends Animal {
    public Dog(String name, String breed) {
        super(name);   // ① 親のコンストラクタを呼ぶ(コンストラクタの最初の行)
        this.breed = breed;
    }

    @Override
    public void eat() {
        super.eat();               // ② 親の eat() を呼ぶ(コンストラクタ以外でも使える)
        System.out.println("(犬らしくガツガツ食べる)");
    }
}

super(...): 親のコンストラクタを呼ぶ。コンストラクタの最初の行でなければならない。
super.メソッド名(): 親クラスの具体的なメソッドを呼ぶ。メソッド内のどこでも使える。


4-2. オーバーライド(@Override)

オーバーライドとは

子クラスで親クラスのメソッドを同じシグネチャで再定義すること。実行時に実際のオブジェクトの型に応じた処理が選ばれる(これがポリモーフィズムの核心)。

class Animal {
    public void sound() { System.out.println("..."); }
}

class Dog extends Animal {
    @Override
    public void sound() { System.out.println("ワンワン"); }  // 親を上書き
}

class Cat extends Animal {
    @Override
    public void sound() { System.out.println("ニャーニャー"); }
}

Animal a = new Dog();
a.sound();  // "ワンワン"(Dog の sound が呼ばれる)

a = new Cat();
a.sound();  // "ニャーニャー"(Cat の sound が呼ばれる)

@Override アノテーション

@Override は省略可能だが、必ず書くべき。コンパイラに「自分はオーバーライドしているつもり」と伝え、スペルミス等があればエラーで気づける。

class Dog extends Animal {
    @Override
    public void sond() { ... }   // ← タイポ(sound → sond)
    // → コンパイルエラー:Animal に sond() はないのでオーバーライドできない
}

// @Override なしだと
class Dog extends Animal {
    public void sond() { ... }   // ← タイポでも「新しいメソッドを定義した」として通る
    // sound() はオーバーライドされていない → 意図した動作にならない
}

オーバーライドの5つのルール(試験最頻出)

ルール1: メソッドシグネチャが同じであること

シグネチャ = メソッド名 + 引数リスト(引数の型・数・順序)。これが完全に一致しなければオーバーライドにならない。

class Parent {
    void method(int x) { }
}
class Child extends Parent {
    void method(int x) { }       // ✅ オーバーライド(同じシグネチャ)
    void method(double x) { }    // ✅ オーバーロード(引数型が違う)
    void method(int x, int y) { } // ✅ オーバーロード(引数数が違う)
}

ルール2: 戻り値型は同じか、より具体的な型(共変戻り値型)でよい

class Animal {
    Animal create() { return new Animal(); }
}
class Dog extends Animal {
    @Override
    Dog create() { return new Dog(); }    // ✅ Dog は Animal のサブクラス
    // Animal create() { ... }           // ✅ 親と同じ型も OK
    // Object create() { ... }           // ❌ Object は Animal より広い型(祖先)
}

これを**共変戻り値型(Covariant Return Type)**という。「より具体的な(子クラス)型に絞れる」ことが許されている。

ルール3: アクセス修飾子は親より広くする方向のみ OK(狭めるのは NG)

「呼び出し側からの可視性を狭くすることはできない」というルール。

class Parent {
    protected void method() { }
}
class Child extends Parent {
    public void method() { }     // ✅ protected → public(広める: OK)
    // void method() { }         // ❌ protected → package-private(狭める: NG)
    // private void method() { } // ❌ protected → private(狭める: NG)
}

なぜ狭められないのか?
Parent p = new Child() という形でアクセスするとき、呼び出し側は p.method()protected レベルでアクセス可能だと信じている。それを private に変えると「呼べるはずだったのに呼べない」という矛盾が起きる。

ルール4: checked 例外は親以下の範囲のみ宣言できる

checked 例外(IOException など)については、オーバーライドしたメソッドで宣言できる例外の範囲が制限される。

class Parent {
    void method() throws IOException { }
}
class Child extends Parent {
    @Override
    void method() throws FileNotFoundException { }  // ✅ FileNotFoundException ⊂ IOException
    // void method() throws IOException { }         // ✅ 同じ例外
    // void method() { }                            // ✅ 例外を減らすのも OK
    // void method() throws Exception { }           // ❌ IOException より広い Exception は NG
    // void method() throws SQLException { }        // ❌ IOException と無関係な例外も NG
}

unchecked 例外(RuntimeException のサブクラス)はこのルールの対象外。新しい unchecked 例外を追加しても問題なし。

ルール5: static・final・private はオーバーライドできない

class Parent {
    static void staticMethod() { System.out.println("Parent static"); }
    final void finalMethod()   { System.out.println("Parent final"); }
    private void privateMethod(){ System.out.println("Parent private"); }
}

class Child extends Parent {
    // static を「オーバーライド」しようとすると → ハイディング(別物)
    static void staticMethod() { System.out.println("Child static"); }

    // final はオーバーライド完全禁止
    // void finalMethod() { }  // ❌ コンパイルエラー

    // private は継承されないので、これは「新しいメソッドの追加」(@Override を付けるとエラー)
    void privateMethod() { System.out.println("Child private"); }
}

4-3. ハイディング vs オーバーライド ─ 最大の罠

static メソッドはオーバーライドではなくハイディング

static メソッドを子クラスで同じ名前で定義することを**ハイディング(Hiding)**という。見た目はオーバーライドに似ているが、動作が全く違う。

決定的な違い: オーバーライドは「実行時の型(動的型)」で決まるが、ハイディングは「変数の宣言型(コンパイル時型)」で決まる。

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

class Child extends Parent {
    static void greet() { System.out.println("Child greet"); }   // ハイディング
    @Override
    void hello() { System.out.println("Child hello"); }           // オーバーライド
}

Parent obj = new Child();
obj.greet();   // "Parent greet"(変数の宣言型 Parent で決まる → ハイディング)
obj.hello();   // "Child hello" (実行時の型 Child で決まる → オーバーライド)

Child c = new Child();
c.greet();     // "Child greet"(変数の宣言型 Child)
c.hello();     // "Child hello"(実行時型 Child)

試験頻出の引っかけ
Parent obj = new Child(); のとき obj.greet() は “Parent” or “Child” どちら?
答えは “Parent”(static はハイディング。変数の型で決まる)

フィールドもハイディング(オーバーライドの対象外)

フィールドもメソッドと同様に、変数の宣言型で決まる。

class Parent {
    String type = "Parent";
    String getType() { return "Parent"; }
}

class Child extends Parent {
    String type = "Child";      // フィールドのハイディング(Hiding)
    @Override
    String getType() { return "Child"; }   // メソッドのオーバーライド
}

Parent p = new Child();
System.out.println(p.type);       // "Parent"(フィールド: 宣言型 Parent で決まる)
System.out.println(p.getType());  // "Child" (メソッド: 実行時型 Child で決まる)

Child c = (Child) p;
System.out.println(c.type);       // "Child" (宣言型が Child になったので)
System.out.println(c.getType());  // "Child"

まとめ:

  • インスタンスメソッド: 実行時型で決まる(ポリモーフィズム)
  • static メソッド: 宣言型で決まる(ハイディング)
  • フィールド: 宣言型で決まる(ハイディング)

4-4. オーバーロード vs オーバーライド

よく混同されるのでまとめておく。

観点オーバーロードオーバーライド
定義場所同クラスまたは継承先サブクラスで親を再定義
メソッド名同じ同じ
引数違う(型・数・順序)同じ
戻り値型自由同じか共変
決まるタイミングコンパイル時(静的ディスパッチ)実行時(動的ディスパッチ)
@Override意味がない付けることを推奨
class Calculator {
    // オーバーロード(同クラス内。引数が違う)
    int add(int a, int b)       { return a + b; }
    double add(double a, double b) { return a + b; }
    int add(int a, int b, int c)   { return a + b + c; }

    // ❌ 戻り値型だけが違うのはオーバーロードにならない(コンパイルエラー)
    // double add(int a, int b) { return a + b; }
}

4-5. ポリモーフィズム ─「同じ操作が、型によって違う動作をする」

コンパイル時型と実行時型の分離

ポリモーフィズムを理解するには「コンパイル時型(変数の宣言型)」と「実行時型(実際のオブジェクトの型)」を区別することが必須。

Animal a = new Dog("ポチ");
//      ↑              ↑
//  コンパイル時型    実行時型
//   = Animal         = Dog

コンパイラは宣言型しか知らない(コンパイル時)。コンパイルエラーかどうかは宣言型をもとに判断される。
JVM は実行時型を知っている(実行時)。どのメソッドが呼ばれるかは実行時型で決まる。

Animal a = new Dog("ポチ");

a.eat();     // ✅ コンパイル OK: Animal に eat() がある
             //    実行時: Dog がオーバーライドしていれば Dog の eat() が呼ばれる

a.bark();    // ❌ コンパイルエラー: Animal に bark() は定義されていない
             //    実行時型が Dog でも、コンパイル時に Animal の定義だけを見る

これが「参照変数の型を超えたメソッドを呼ぼうとするとコンパイルエラーになる」理由。

ポリモーフィズムの実用例

Animal[] animals = {
    new Dog("ポチ"),
    new Cat("ミケ"),
    new Dog("ハチ"),
    new Cat("タマ")
};

// 全員に eat() を呼ぶ。それぞれの型の実装が呼ばれる
for (Animal a : animals) {
    a.eat();   // Dog の eat() か Cat の eat() かは実行時に決まる
}

これが「同じコードが、異なる型のオブジェクトに対して適切な動作をする」ポリモーフィズムの威力。新しい動物クラス(Bird など)を追加しても、このループを変更する必要がない。

アップキャストとダウンキャスト

アップキャスト(子 → 親): 自動で行われる。制限が「減る」方向。安全。

Dog d = new Dog("ポチ");
Animal a = d;         // ✅ 自動(明示的キャスト不要)
Object o = d;         // ✅ すべてのクラスは Object のサブクラスなので OK

ダウンキャスト(親 → 子): 明示的キャストが必要。制限が「増える」方向。失敗すると例外。

Animal a = new Dog("ポチ");
Dog d = (Dog) a;      // ✅ a の実行時型が Dog なので OK
d.bark();

Animal a2 = new Cat("ミケ");
Dog d2 = (Dog) a2;    // ❌ 実行時 ClassCastException!Cat を Dog にはキャストできない
                      // コンパイルは通る(Animal と Dog の間に継承関係がある)が実行時エラー

ダウンキャスト前に instanceof で型を確認するのが安全な書き方。

Animal a = new Dog("ポチ");

if (a instanceof Dog) {
    Dog d = (Dog) a;   // 確認後のキャスト(ClassCastException なし)
    d.bark();
}

// Java 16+ パターンマッチング(型確認とキャストを1行に)
if (a instanceof Dog d) {
    d.bark();
}

4-6. 抽象クラス(abstract class)

なぜ抽象クラスが必要か

「すべての動物は eat() できる」けど「Animal そのものをインスタンス化するのはおかしい(「動物」という抽象的な概念のインスタンスが存在するのは変)」という場面に使う。

abstract クラスはインスタンス化を禁止する代わりに、サブクラスへ実装を強制する仕組み。

public abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象メソッド: 宣言だけ。中身は書かない。サブクラスが必ず実装する
    public abstract double area();
    public abstract double perimeter();

    // 具体メソッド: 共通実装を提供(サブクラスで使い回せる)
    public void printInfo() {
        System.out.printf("%s の %s: 面積=%.2f%n", color, getClass().getSimpleName(), area());
    }
}
public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() { return Math.PI * radius * radius; }

    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width  = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }
}

抽象クラスのルール(全4つ)

ルール1: abstract クラスはインスタンス化できない

Shape s = new Shape("赤");         // ❌ コンパイルエラー
Shape s = new Circle("赤", 5.0);  // ✅ サブクラスはOK(Shape 型変数に代入可)

ルール2: 抽象メソッドを持つクラスは必ず abstract にしなければならない

class Bad {
    abstract void method();  // ❌ クラスが abstract でないのに抽象メソッドはエラー
}
abstract class Good {
    abstract void method();  // ✅
}

ルール3: 具体クラス(abstract でないクラス)がすべての抽象メソッドを実装しなければならない

class IncompleteShape extends Shape {
    // area() は実装したが perimeter() を実装していない
    @Override
    public double area() { return 0; }
    // perimeter() を実装しないなら IncompleteShape も abstract にする必要がある
}
// ❌ コンパイルエラー: perimeter() が未実装

ルール4: abstract クラスが abstract クラスを継承するとき、実装を先送りできる(中間抽象クラス)

abstract class A {
    abstract void doA();
    abstract void doB();
}

abstract class B extends A {
    @Override
    public void doA() { System.out.println("B が doA を実装"); }
    // doB() はまだ abstract のまま OK(B も abstract だから)
}

class C extends B {
    @Override
    public void doB() { System.out.println("C が doB を実装"); }
    // doA() は B がすでに実装しているので不要
    // → C はすべての抽象メソッドが実装されている → 具体クラスとして OK
}

ルール5: abstract クラスはコンストラクタを持てる

new できないのにコンストラクタを書ける。これはサブクラスのコンストラクタが super(...) で呼ぶためだ。


4-7. final ─ 変更を禁止する3種類の使い方

final は「もうここで固定(変更不可)」を宣言する修飾子。付ける場所によって意味が変わる。

final クラス ─ 継承禁止

public final class String { }     // Java 標準ライブラリ(継承されないよう保護)

class MyClass extends String { }  // ❌ コンパイルエラー

いつ使うか: イミュータブルクラスの実装(Chapter 03)・シングルトンパターン・継承による「横取り」を防ぎたいセキュリティ上重要なクラス。

final メソッド ─ オーバーライド禁止

class Template {
    public final void execute() {    // final: サブクラスで変えさせない
        preProcess();
        doExecute();                 // これだけはサブクラスで変えられる
        postProcess();
    }

    protected void preProcess()  { }
    protected void doExecute()   { }
    protected void postProcess() { }
}

class Subclass extends Template {
    // execute() はオーバーライドできない
    @Override
    protected void doExecute() { System.out.println("ここは変えられる"); }
}

これは「テンプレートメソッドパターン」。処理の骨格を親が final で固定し、詳細だけをサブクラスに委ねる設計。

final 変数・フィールド ─ 再代入禁止

final int MAX = 100;
// MAX = 200;  // ❌ コンパイルエラー

// final フィールド
class Config {
    final String name;   // 宣言時に初期化しない場合はコンストラクタで初期化必須

    Config(String name) {
        this.name = name;  // ← コンストラクタでのみ設定可能(ここが最後のチャンス)
    }

    void changeConfig() {
        // this.name = "other";  // ❌ コンストラクタ以外では変更不可
    }
}

試験頻出
final フィールドは「宣言時の初期値」か「コンストラクタ」のどちらかで初期化しなければならない。両方で初期化しようとしてもエラー(2回代入になる)。どちらでも初期化されない場合もエラー。

class Config {
    final int x = 10;     // OK: 宣言時に初期化
    final int y;          // OK: コンストラクタで初期化する予定

    Config() {
        y = 20;           // OK
        // x = 30;        // ❌ すでに初期化済みなので再代入不可
    }
}

4-8. Object クラスのメソッド

すべてのクラスは Object を継承するため、以下のメソッドは必ず持っている。

toString()

オブジェクトを文字列として表現する。System.out.println(obj)obj.toString() を呼ぶ。

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point p = new Point(3, 4);
System.out.println(p);  // "Point@6d06d69c"(クラス名@ハッシュ。読めない)

// オーバーライドして可読性を上げる
class Point {
    @Override
    public String toString() { return "(" + x + ", " + y + ")"; }
}
System.out.println(p);  // "(3, 4)"

equals() ─ 同一性 vs 等価性

  • ==(同一性): 同じオブジェクトか(参照が同じか)を確認
  • equals()(等価性): 論理的に「等しい」かを確認(内容比較)

Objectequals() はデフォルトで == と同じ動作(参照比較)。意味のある比較をするにはオーバーライドが必要。

class Person {
    String name;
    int age;
    Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                          // 同じ参照なら true
        if (!(o instanceof Person other)) return false;      // 型確認
        return age == other.age && name.equals(other.name); // フィールド比較
    }
}

Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);  // 別オブジェクトだが内容は同じ

System.out.println(p1 == p2);       // false(別オブジェクト)
System.out.println(p1.equals(p2));  // true(内容が同じ。オーバーライドしているので)

equals と hashCode の契約

equals() をオーバーライドしたら必ず hashCode() もオーバーライドする

契約: a.equals(b)true なら a.hashCode() == b.hashCode() でなければならない。

HashMapHashSet は「ハッシュコードで大まかに場所を特定し、equals で確定する」という仕組み。この契約が守られていないと正しく動作しない。

// hashCode をオーバーライドしない場合の問題
class BadPerson {
    String name;
    // equals はオーバーライドしたが hashCode は未実装
}

Set<BadPerson> set = new HashSet<>();
set.add(new BadPerson("Alice"));

// equals では true になるはずだが...
System.out.println(set.contains(new BadPerson("Alice")));  // false になり得る!
// → ハッシュコードが違うバケットを見てしまい、見つからない

✏️ 練習問題

次のコードの出力を答えよ。

class Parent {
    static String type = "Parent";
    String name = "parent-name";

    static void staticShow() { System.out.println("static: " + type); }
    void show() { System.out.println("instance: " + name); }
}

class Child extends Parent {
    static String type = "Child";
    String name = "child-name";

    static void staticShow() { System.out.println("static: " + type); }
    @Override
    void show() { System.out.println("instance: " + name); }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.staticShow();   // 1
        p.show();         // 2
        System.out.println(p.type);  // 3
        System.out.println(p.name);  // 4
    }
}
答え
static: Parent    // 1: static はハイディング。宣言型 Parent の staticShow が呼ばれる
instance: child-name  // 2: インスタンスメソッドは実行時型 Child のが呼ばれる
Parent            // 3: static フィールドは宣言型 Parent の type
parent-name       // 4: インスタンスフィールドも宣言型 Parent の name
  • 静的メソッド・フィールド(static 含む): 宣言型で決まる
  • インスタンスメソッド: 実行時型で決まる(ポリモーフィズム)
  • インスタンスフィールド: 宣言型で決まる(ハイディング)

Chapter 04 まとめチェックリスト

  • extends で継承。クラスの継承は1つだけ(単一継承)
  • super() はコンストラクタの最初の行のみ
  • super.メソッド名() で親クラスのオーバーライドされたメソッドを呼べる
  • オーバーライドの5ルール: シグネチャ同一・共変戻り値型・修飾子は広げる方向のみ・checked例外は同等以下・static/final/privateはオーバーライド不可
  • @Override は省略可だが書くことを強く推奨(タイポ検出)
  • ポリモーフィズム: インスタンスメソッドは実行時型で決まる
  • static メソッドはハイディング: 宣言型で決まる(ポリモーフィズムの対象外)
  • フィールドはハイディング: 宣言型で決まる(ポリモーフィズムの対象外)
  • ダウンキャスト失敗 → 実行時 ClassCastException(コンパイルは通る)
  • null instanceof Anything → false(例外にならない)
  • 抽象クラスは new 不可。コンストラクタは持てる
  • 具体クラスは全抽象メソッドを実装しなければならない
  • abstract クラスが abstract クラスを継承する場合、実装の先送りが可能
  • final クラス=継承禁止、final メソッド=オーバーライド禁止、final 変数=再代入禁止
  • final フィールドは宣言時かコンストラクタで初期化(最大1回のみ)
  • equals() をオーバーライドしたら hashCode() も必ずオーバーライドする