【Java】メソッドのオーバーライドとポリモーフィズムの違いを解説

Java

こんにちは、かつコーチです。
前回は継承の基本を解説しました。
今回は、継承と組み合わせて使う「オーバーライド」と、オブジェクト指向の中核概念の1つである「ポリモーフィズム」について解説します。

オーバーライドとは?

親クラスのメソッドを子クラスで上書きする

オーバーライドとは、親クラスで定義されたメソッドを、子クラスで同じシグネチャ(メソッド名・引数の型と数)のまま再定義することです。
子クラス独自の処理内容に置き換えたいときに使います。

public class Animal {
    protected String name;

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

    public void makeSound() {
        System.out.println(name + "が鳴きました");
    }
}
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + "がワンと吠えました");
    }
}
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + "がニャーと鳴きました");
    }
}

@Overrideアノテーションは必須ではありませんが、「これはオーバーライドのつもりです」とコンパイラに伝えることで、意図しないミス(メソッド名のタイプミスなど)をコンパイル時に検出してもらえます。

オーバーロードとの違い

似た名前の「オーバーロード」と混同しやすいので整理しておきます。

オーバーライドオーバーロード
関係親クラスと子クラス間同じクラス内
メソッド名同じ同じ
引数同じ(変えられない)異なる(型・数を変える)
目的親の動作を子クラス独自の動作に置き換える同じ処理を異なる引数パターンで提供する
// オーバーロードの例:同じクラス内で引数違いのメソッドを複数定義
public class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

名前が似ているため、初学者はよく混同しますが、「クラスをまたぐか、同じクラス内か」で見分けると分かりやすいです。

ポリモーフィズムとは?

「多態性」=同じ呼び方で異なる動作をさせる

ポリモーフィズム(多態性)とは、同じ型として扱いながら、実際のオブジェクトの種類によって異なる動作をさせられる性質のことです。
先ほどのAnimalDogCatの例を使うと、以下のようなコードが書けます。

public class Main {
    public static void main(String[] args) {
        Animal[] animals = {
            new Dog("ポチ"),
            new Cat("タマ"),
        };

        for (Animal animal : animals) {
            animal.makeSound(); // 呼び出し方は同じでも、実行される処理が異なる
        }
    }
}

実行結果は以下のようになります。

ポチがワンと吠えました
タマがニャーと鳴きました

animal.makeSound()という同じ呼び出し方をしているにもかかわらず、animalの実体がDogCatかによって実行される処理が変わっています。
これがポリモーフィズムの効果です。

なぜポリモーフィズムが必要なのか

ポリモーフィズムを使わない場合、動物の種類ごとにif文で分岐する必要があります。

❌Before

public class Main {
    public static void main(String[] args) {
        String[] types = {"dog", "cat"};
        String[] names = {"ポチ", "タマ"};

        for (int i = 0; i < types.length; i++) {
            if (types[i].equals("dog")) {
                System.out.println(names[i] + "がワンと吠えました");
            } else if (types[i].equals("cat")) {
                System.out.println(names[i] + "がニャーと鳴きました");
            }
            // 新しい動物を追加するたびにここが増え続ける
        }
    }
}

✅After

public class Main {
    public static void main(String[] args) {
        Animal[] animals = { new Dog("ポチ"), new Cat("タマ") };

        for (Animal animal : animals) {
            animal.makeSound(); // 新しい動物クラスが増えてもこのループは変更不要
        }
    }
}

新しい動物(例えばBirdクラス)を追加したい場合、Beforeではif文の分岐を増やす必要がありますが、AfterではAnimalを継承した新しいクラスを作るだけで済みます。
呼び出し側のコードを変更せずに機能を拡張できる点が、ポリモーフィズムの大きなメリットです。

よくあるつまずきポイント・エラー対処

オーバーライドのつもりがオーバーロードになっていた

私が実際にハマった経験として、引数の型を微妙に間違えたせいで、オーバーライドではなく意図せずオーバーロードになってしまったケースがあります。

❌Before

public class Animal {
    public void makeSound(String prefix) {
        System.out.println(prefix + "鳴きました");
    }
}

public class Dog extends Animal {
    // 引数の型をStringからObjectに書き間違えた
    public void makeSound(Object prefix) {
        System.out.println(prefix + "ワンと吠えました");
    }
}
Animal animal = new Dog();
animal.makeSound("ポチが"); // Animalのmakeソウンドが呼ばれてしまう(意図と違う)

@Overrideを付けていれば、このミスは以下のようなコンパイルエラーとしてすぐに検出できました。

error: method does not override or implement a method from a supertype

✅After

public class Dog extends Animal {
    @Override // これを付けることで引数の型ミスがコンパイル時に発覚する
    public void makeSound(String prefix) {
        System.out.println(prefix + "ワンと吠えました");
    }
}

この経験から、オーバーライドするメソッドには必ず@Overrideを付けることを習慣にしています。
コンパイラがミスを早期発見してくれる、非常に費用対効果の高い一手間です。

親クラスの型で呼び出せるのは親クラスに定義されたメソッドだけ

Animal animal = new Dog("ポチ");
animal.bark(); // コンパイルエラー:Animal型にbarkは定義されていない

animalの実体はDogですが、変数の型はAnimalです。
呼び出せるメソッドは、変数の型(Animal)に定義されているものに限られます。
Dog固有のメソッドを呼びたい場合は、前回の記事で紹介したinstanceofによるキャストが必要です。

応用・一歩先の使い方

メソッドの引数として親クラス型を受け取る

ポリモーフィズムを活かすと、メソッドの引数を親クラス型にしておくことで、あらゆる子クラスのオブジェクトを1つのメソッドで受け取れます。

public class AnimalShelter {
    public void feed(Animal animal) {
        System.out.println(animal.name + "にごはんをあげました");
        animal.makeSound();
    }
}
AnimalShelter shelter = new AnimalShelter();
shelter.feed(new Dog("ポチ"));
shelter.feed(new Cat("タマ"));

feedメソッドはDogCatを意識せず、Animalという共通の型だけを知っていればよくなります。
これは次に学ぶ「抽象クラス」や「interface」を使った設計にも直結する、重要な考え方です。

まとめ

この記事のポイント

  • オーバーライドは親クラスのメソッドを子クラスで同じシグネチャのまま上書きすること。@Overrideを必ず付ける
  • オーバーロードは同じクラス内で引数違いのメソッドを複数定義すること。オーバーライドとは別物
  • ポリモーフィズムは同じ呼び出し方でも実体の型によって動作が変わる性質で、if分岐を減らし拡張性を高める
  • 親クラス型の変数からは、親クラスに定義されたメソッドしか呼び出せない

次に読むべき記事

ポリモーフィズムの理解が深まったら、次はインスタンス化できない特殊なクラスである「抽象クラス(abstract)の使い方」に進みましょう。

タグ: Java, 中級者向け, オブジェクト指向

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